浏览代码

Merge pull request #4 from jMonkeyEngine/master

Merge branch "jmonkeyengine/master" into "scenecomposer/master"
Dokthar 10 年之前
父节点
当前提交
d48c1e0e9d
共有 100 个文件被更改,包括 6785 次插入3043 次删除
  1. 100 174
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java
  2. 0 686
      jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java
  3. 147 172
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  4. 159 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java
  5. 24 43
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java
  6. 108 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java
  7. 416 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java
  8. 0 140
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java
  9. 11 6
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java
  10. 24 24
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  11. 0 257
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java
  12. 475 0
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  13. 30 46
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java
  14. 3 0
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
  15. 11 15
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  16. 16 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  17. 1 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java
  18. 20 0
      jme3-core/src/main/java/com/jme3/animation/Bone.java
  19. 19 0
      jme3-core/src/main/java/com/jme3/math/FastMath.java
  20. 20 0
      jme3-core/src/main/java/com/jme3/math/Transform.java
  21. 13 1
      jme3-core/src/main/java/com/jme3/renderer/Caps.java
  22. 2 1
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java
  23. 7 3
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
  24. 23 3
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java
  25. 8 6
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugES.java
  26. 1 3
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java
  27. 2 1
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLFbo.java
  28. 24 14
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java
  29. 78 88
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  30. 122 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLTiming.java
  31. 41 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLTimingState.java
  32. 3 4
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java
  33. 1 0
      jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java
  34. 14 10
      jme3-core/src/main/java/com/jme3/texture/Image.java
  35. 11 9
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.frag
  36. 8 5
      jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert
  37. 13 6
      jme3-core/src/main/resources/joystick-mapping.properties
  38. 7 5
      jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java
  39. 12 11
      jme3-core/src/plugins/java/com/jme3/texture/plugins/DXTFlipper.java
  40. 2 2
      jme3-effects/src/main/java/com/jme3/post/ssao/SSAOFilter.java
  41. 1 5
      jme3-effects/src/main/resources/Common/MatDefs/Water/Water.frag
  42. 1 0
      jme3-effects/src/main/resources/Common/MatDefs/Water/Water.j3md
  43. 4 0
      jme3-ios/src/main/java/com/jme3/asset/IOS.cfg
  44. 0 1054
      jme3-ios/src/main/java/com/jme3/audio/android/AL.java
  45. 0 67
      jme3-ios/src/main/java/com/jme3/audio/android/AndroidAudioData.java
  46. 53 0
      jme3-ios/src/main/java/com/jme3/audio/ios/IosAL.java
  47. 26 0
      jme3-ios/src/main/java/com/jme3/audio/ios/IosALC.java
  48. 32 0
      jme3-ios/src/main/java/com/jme3/audio/ios/IosEFX.java
  49. 0 20
      jme3-ios/src/main/java/com/jme3/audio/plugins/AndroidAudioLoader.java
  50. 7 1
      jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java
  51. 3 2
      jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java
  52. 9 6
      jme3-ios/src/main/java/com/jme3/system/ios/IosImageLoader.java
  53. 13 2
      jme3-ios/src/main/java/com/jme3/system/ios/JmeIosSystem.java
  54. 8 0
      jme3-ios/src/main/resources/com/jme3/asset/IOS.cfg
  55. 14 1
      jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
  56. 8 60
      jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java
  57. 98 0
      jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java
  58. 96 0
      jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java
  59. 28 7
      jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java
  60. 78 0
      jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java
  61. 8 0
      jme3-networking/src/main/java/com/jme3/network/Client.java
  62. 8 0
      jme3-networking/src/main/java/com/jme3/network/Server.java
  63. 27 2
      jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java
  64. 53 5
      jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java
  65. 188 0
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  66. 51 0
      jme3-networking/src/main/java/com/jme3/network/service/AbstractClientService.java
  67. 70 0
      jme3-networking/src/main/java/com/jme3/network/service/AbstractHostedService.java
  68. 111 0
      jme3-networking/src/main/java/com/jme3/network/service/AbstractService.java
  69. 72 0
      jme3-networking/src/main/java/com/jme3/network/service/ClientService.java
  70. 100 0
      jme3-networking/src/main/java/com/jme3/network/service/ClientServiceManager.java
  71. 74 0
      jme3-networking/src/main/java/com/jme3/network/service/HostedService.java
  72. 140 0
      jme3-networking/src/main/java/com/jme3/network/service/HostedServiceManager.java
  73. 67 0
      jme3-networking/src/main/java/com/jme3/network/service/Service.java
  74. 160 0
      jme3-networking/src/main/java/com/jme3/network/service/ServiceManager.java
  75. 123 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcClientService.java
  76. 260 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java
  77. 52 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcHandler.java
  78. 227 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcHostedService.java
  79. 98 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcCallMessage.java
  80. 89 0
      jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java
  81. 69 0
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java
  82. 72 0
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java
  83. 314 0
      jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java
  84. 72 0
      jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java
  85. 103 0
      jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java
  86. 9 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/ContentTextureKey.java
  87. 413 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/FbxLoader.java
  88. 42 42
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/SceneLoader.java
  89. 144 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurve.java
  90. 147 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurveNode.java
  91. 82 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimLayer.java
  92. 111 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimStack.java
  93. 44 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimUtil.java
  94. 103 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxBindPose.java
  95. 98 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxCluster.java
  96. 94 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxLimbNode.java
  97. 66 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxSkinDeformer.java
  98. 202 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxToJmeTrack.java
  99. 43 33
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java
  100. 124 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxElement.java

+ 100 - 174
jme3-android/src/main/java/com/jme3/input/android/AndroidGestureHandler.java → jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java

@@ -35,314 +35,240 @@ package com.jme3.input.android;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
-import android.view.View;
-import com.jme3.input.event.InputEvent;
-import com.jme3.input.event.MouseMotionEvent;
 import com.jme3.input.event.TouchEvent;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
  * AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents
- * for gestures.  This class is designed to handle the gestures supported 
+ * for gestures.  This class is designed to handle the gestures supported
  * on Android rev 9 (Android 2.3).  Extend this class to add functionality
  * added by Android after rev 9.
- * 
+ *
  * @author iwgeric
  */
-public class AndroidGestureHandler implements 
-        GestureDetector.OnGestureListener, 
+public class AndroidGestureProcessor implements
+        GestureDetector.OnGestureListener,
         GestureDetector.OnDoubleTapListener,
         ScaleGestureDetector.OnScaleGestureListener {
-    private static final Logger logger = Logger.getLogger(AndroidGestureHandler.class.getName());
-    private AndroidInputHandler androidInput;
-    private GestureDetector gestureDetector;
-    private ScaleGestureDetector scaleDetector;
+    private static final Logger logger = Logger.getLogger(AndroidGestureProcessor.class.getName());
+
+    private AndroidTouchInput touchInput;
     float gestureDownX = -1f;
     float gestureDownY = -1f;
     float scaleStartX = -1f;
     float scaleStartY = -1f;
 
-    public AndroidGestureHandler(AndroidInputHandler androidInput) {
-        this.androidInput = androidInput;
-    }
-    
-    public void initialize() {
-    }
-    
-    public void destroy() {
-        setView(null);
-    }
-    
-    public void setView(View view) {
-        if (view != null) {
-            gestureDetector = new GestureDetector(view.getContext(), this);
-            scaleDetector = new ScaleGestureDetector(view.getContext(), this);
-        } else {
-            gestureDetector = null;
-            scaleDetector = null;
-        }
-    }
-    
-    public void detectGesture(MotionEvent event) {
-        if (gestureDetector != null && scaleDetector != null) {
-            gestureDetector.onTouchEvent(event);
-            scaleDetector.onTouchEvent(event);
-        }
-    }
-
-    private int getPointerIndex(MotionEvent event) {
-        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
-                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-    }
-    
-    private int getPointerId(MotionEvent event) {
-        return event.getPointerId(getPointerIndex(event));
+    public AndroidGestureProcessor(AndroidTouchInput touchInput) {
+        this.touchInput = touchInput;
     }
-    
-    private void processEvent(TouchEvent event) {
-        // Add the touch event
-        androidInput.addEvent(event);
-        if (androidInput.isSimulateMouse()) {
-            InputEvent mouseEvent = generateMouseEvent(event);
-            if (mouseEvent != null) {
-                // Add the mouse event
-                androidInput.addEvent(mouseEvent);
-            }
-        }
-    }
-
-    // TODO: Ring Buffer for mouse events?
-    private InputEvent generateMouseEvent(TouchEvent event) {
-        InputEvent inputEvent = null;
-        int newX;
-        int newY;
-        int newDX;
-        int newDY;
 
-        if (androidInput.isMouseEventsInvertX()) {
-            newX = (int) (androidInput.invertX(event.getX()));
-            newDX = (int)event.getDeltaX() * -1;
-        } else {
-            newX = (int) event.getX();
-            newDX = (int)event.getDeltaX();
-        }
-        int wheel = (int) (event.getScaleSpan()); // might need to scale to match mouse wheel
-        int dWheel = (int) (event.getDeltaScaleSpan()); // might need to scale to match mouse wheel
-
-        if (androidInput.isMouseEventsInvertY()) {
-            newY = (int) (androidInput.invertY(event.getY()));
-            newDY = (int)event.getDeltaY() * -1;
-        } else {
-            newY = (int) event.getY();
-            newDY = (int)event.getDeltaY();
-        }
-
-        switch (event.getType()) {
-            case SCALE_MOVE:
-                inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, wheel, dWheel);
-                inputEvent.setTime(event.getTime());
-                break;
-        }
-
-        return inputEvent;
-    }
-    
     /* Events from onGestureListener */
-    
+
+    @Override
     public boolean onDown(MotionEvent event) {
         // start of all GestureListeners.  Not really a gesture by itself
         // so we don't create an event.
         // However, reset the scaleInProgress here since this is the beginning
         // of a series of gesture events.
-//        logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-        gestureDownX = androidInput.getJmeX(event.getX());
-        gestureDownY = androidInput.invertY(androidInput.getJmeY(event.getY()));
+//        logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        gestureDownX = touchInput.getJmeX(event.getX());
+        gestureDownY = touchInput.invertY(touchInput.getJmeY(event.getY()));
         return true;
     }
 
+    @Override
     public boolean onSingleTapUp(MotionEvent event) {
         // Up of single tap.  May be followed by a double tap later.
         // use onSingleTapConfirmed instead.
-//        logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
+//        logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
         return true;
     }
 
+    @Override
     public void onShowPress(MotionEvent event) {
-//        logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-        float jmeX = androidInput.getJmeX(event.getX());
-        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+//        logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        float jmeX = touchInput.getJmeX(event.getX());
+        float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0);
-        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setPointerId(touchInput.getPointerId(event));
         touchEvent.setTime(event.getEventTime());
         touchEvent.setPressure(event.getPressure());
-        processEvent(touchEvent);
+        touchInput.addEvent(touchEvent);
     }
 
+    @Override
     public void onLongPress(MotionEvent event) {
-//        logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-        float jmeX = androidInput.getJmeX(event.getX());
-        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+//        logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        float jmeX = touchInput.getJmeX(event.getX());
+        float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0);
-        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setPointerId(touchInput.getPointerId(event));
         touchEvent.setTime(event.getEventTime());
         touchEvent.setPressure(event.getPressure());
-        processEvent(touchEvent);
+        touchInput.addEvent(touchEvent);
     }
 
+    @Override
     public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) {
         // if not scaleInProgess, send scroll events.  This is to avoid sending
         // scroll events when one of the fingers is lifted just before the other one.
         // Avoids sending the scroll for that brief period of time.
         // Return true so that the next event doesn't accumulate the distX and distY values.
-        // Apparantly, both distX and distY are negative.  
+        // Apparantly, both distX and distY are negative.
         // Negate distX to get the real value, but leave distY negative to compensate
         // for the fact that jME has y=0 at bottom where Android has y=0 at top.
-//        if (!scaleInProgress) {
-        if (!scaleDetector.isInProgress()) {
-//            logger.log(Level.INFO, "onScroll pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, dx: {7}, dy: {8}", 
-//                    new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY});
+        if (!touchInput.getScaleDetector().isInProgress()) {
+//            logger.log(Level.INFO, "onScroll pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, dx: {7}, dy: {8}",
+//                    new Object[]{touchInput.getPointerId(startEvent), touchInput.getAction(startEvent), startEvent.getX(), startEvent.getY(), touchInput.getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY});
 
-            float jmeX = androidInput.getJmeX(endEvent.getX());
-            float jmeY = androidInput.invertY(androidInput.getJmeY(endEvent.getY()));
-            TouchEvent touchEvent = androidInput.getFreeTouchEvent();
-            touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, androidInput.getJmeX(-distX), androidInput.getJmeY(distY));
-            touchEvent.setPointerId(getPointerId(endEvent));
+            float jmeX = touchInput.getJmeX(endEvent.getX());
+            float jmeY = touchInput.invertY(touchInput.getJmeY(endEvent.getY()));
+            TouchEvent touchEvent = touchInput.getFreeTouchEvent();
+            touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, touchInput.getJmeX(-distX), touchInput.getJmeY(distY));
+            touchEvent.setPointerId(touchInput.getPointerId(endEvent));
             touchEvent.setTime(endEvent.getEventTime());
             touchEvent.setPressure(endEvent.getPressure());
-            processEvent(touchEvent);
+            touchInput.addEvent(touchEvent);
         }
         return true;
     }
 
+    @Override
     public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) {
         // Fling happens only once at the end of the gesture (all fingers up).
         // Fling returns the velocity of the finger movement in pixels/sec.
         // Therefore, the dX and dY values are actually velocity instead of distance values
         // Since this does not track the movement, use the start position and velocity values.
-        
-//        logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}", 
-//                new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY});
 
-        float jmeX = androidInput.getJmeX(startEvent.getX());
-        float jmeY = androidInput.invertY(androidInput.getJmeY(startEvent.getY()));
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+//        logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}",
+//                new Object[]{touchInput.getPointerId(startEvent), touchInput.getAction(startEvent), startEvent.getX(), startEvent.getY(), touchInput.getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY});
+
+        float jmeX = touchInput.getJmeX(startEvent.getX());
+        float jmeY = touchInput.invertY(touchInput.getJmeY(startEvent.getY()));
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY);
-        touchEvent.setPointerId(getPointerId(endEvent));
+        touchEvent.setPointerId(touchInput.getPointerId(endEvent));
         touchEvent.setTime(endEvent.getEventTime());
         touchEvent.setPressure(endEvent.getPressure());
-        processEvent(touchEvent);
+        touchInput.addEvent(touchEvent);
         return true;
     }
 
     /* Events from onDoubleTapListener */
-    
+
+    @Override
     public boolean onSingleTapConfirmed(MotionEvent event) {
         // Up of single tap when no double tap followed.
-//        logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-        float jmeX = androidInput.getJmeX(event.getX());
-        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+//        logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        float jmeX = touchInput.getJmeX(event.getX());
+        float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0);
-        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setPointerId(touchInput.getPointerId(event));
         touchEvent.setTime(event.getEventTime());
         touchEvent.setPressure(event.getPressure());
-        processEvent(touchEvent);
+        touchInput.addEvent(touchEvent);
         return true;
     }
 
+    @Override
     public boolean onDoubleTap(MotionEvent event) {
         //The down motion event of the first tap of the double-tap
-        // We could use this event to fire off a double tap event, or use 
+        // We could use this event to fire off a double tap event, or use
         // DoubleTapEvent with a check for the UP action
-//        logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-        float jmeX = androidInput.getJmeX(event.getX());
-        float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY()));
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+//        logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        float jmeX = touchInput.getJmeX(event.getX());
+        float jmeY = touchInput.invertY(touchInput.getJmeY(event.getY()));
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0);
-        touchEvent.setPointerId(getPointerId(event));
+        touchEvent.setPointerId(touchInput.getPointerId(event));
         touchEvent.setTime(event.getEventTime());
         touchEvent.setPressure(event.getPressure());
-        processEvent(touchEvent);
+        touchInput.addEvent(touchEvent);
         return true;
     }
 
+    @Override
     public boolean onDoubleTapEvent(MotionEvent event) {
         //Notified when an event within a double-tap gesture occurs, including the down, move(s), and up events.
         // this means it will get called multiple times for a single double tap
-//        logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}", 
-//                new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()});
-//        if (getAction(event) == MotionEvent.ACTION_UP) {
-//            TouchEvent touchEvent = touchEventPool.getNextFreeEvent();
-//            touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), androidInput.invertY(event.getY()), 0, 0);
-//            touchEvent.setPointerId(getPointerId(event));
-//            touchEvent.setTime(event.getEventTime());
-//            touchEvent.setPressure(event.getPressure());
-//            processEvent(touchEvent);
-//        }
+//        logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}",
+//                new Object[]{touchInput.getPointerId(event), touchInput.getAction(event), event.getX(), event.getY()});
+        if (touchInput.getAction(event) == MotionEvent.ACTION_UP) {
+            TouchEvent touchEvent = touchInput.getFreeTouchEvent();
+            touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), touchInput.invertY(event.getY()), 0, 0);
+            touchEvent.setPointerId(touchInput.getPointerId(event));
+            touchEvent.setTime(event.getEventTime());
+            touchEvent.setPressure(event.getPressure());
+            touchInput.addEvent(touchEvent);
+        }
         return true;
     }
 
     /* Events from ScaleGestureDetector */
-    
+
+    @Override
     public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
         // Scale uses a focusX and focusY instead of x and y.  Focus is the middle
         // of the fingers.  Therefore, use the x and y values from the Down event
         // so that the x and y values don't jump to the middle position.
         // return true or all gestures for this beginning event will be discarded
-        logger.log(Level.INFO, "onScaleBegin");
+//        logger.log(Level.INFO, "onScaleBegin");
         scaleStartX = gestureDownX;
         scaleStartY = gestureDownY;
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f);
         touchEvent.setPointerId(0);
         touchEvent.setTime(scaleGestureDetector.getEventTime());
         touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
         touchEvent.setDeltaScaleSpan(0f);
         touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
-        processEvent(touchEvent);
-        
+        touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
+        touchInput.addEvent(touchEvent);
+
         return true;
     }
 
+    @Override
     public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
         // return true or all gestures for this event will be accumulated
-        logger.log(Level.INFO, "onScale");
+//        logger.log(Level.INFO, "onScale");
         scaleStartX = gestureDownX;
         scaleStartY = gestureDownY;
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f);
         touchEvent.setPointerId(0);
         touchEvent.setTime(scaleGestureDetector.getEventTime());
         touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
         touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
         touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
-        processEvent(touchEvent);
+        touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
+        touchInput.addEvent(touchEvent);
         return true;
     }
 
+    @Override
     public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
-        logger.log(Level.INFO, "onScaleEnd");
+//        logger.log(Level.INFO, "onScaleEnd");
         scaleStartX = gestureDownX;
         scaleStartY = gestureDownY;
-        TouchEvent touchEvent = androidInput.getFreeTouchEvent();
+        TouchEvent touchEvent = touchInput.getFreeTouchEvent();
         touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f);
         touchEvent.setPointerId(0);
         touchEvent.setTime(scaleGestureDetector.getEventTime());
         touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan());
         touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
         touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress());
-        processEvent(touchEvent);
+        touchEvent.setScaleSpanInProgress(touchInput.getScaleDetector().isInProgress());
+        touchInput.addEvent(touchEvent);
     }
 }

+ 0 - 686
jme3-android/src/main/java/com/jme3/input/android/AndroidInput.java

@@ -1,686 +0,0 @@
-package com.jme3.input.android;
-
-import android.view.*;
-import com.jme3.input.KeyInput;
-import com.jme3.input.RawInputListener;
-import com.jme3.input.TouchInput;
-import com.jme3.input.event.MouseButtonEvent;
-import com.jme3.input.event.MouseMotionEvent;
-import com.jme3.input.event.TouchEvent;
-import com.jme3.input.event.TouchEvent.Type;
-import com.jme3.math.Vector2f;
-import com.jme3.system.AppSettings;
-import com.jme3.util.RingBuffer;
-import java.util.HashMap;
-import java.util.logging.Logger;
-
-/**
- * <code>AndroidInput</code> is one of the main components that connect jme with android. Is derived from GLSurfaceView and handles all Inputs
- * @author larynx
- *
- */
-public class AndroidInput implements
-        TouchInput,
-        View.OnTouchListener,
-        View.OnKeyListener,
-        GestureDetector.OnGestureListener,
-        GestureDetector.OnDoubleTapListener,
-        ScaleGestureDetector.OnScaleGestureListener {
-
-    final private static int MAX_EVENTS = 1024;
-    // Custom settings
-    public boolean mouseEventsEnabled = true;
-    public boolean mouseEventsInvertX = false;
-    public boolean mouseEventsInvertY = false;
-    public boolean keyboardEventsEnabled = false;
-    public boolean dontSendHistory = false;
-    // Used to transfer events from android thread to GLThread
-    final private RingBuffer<TouchEvent> eventQueue = new RingBuffer<TouchEvent>(MAX_EVENTS);
-    final private RingBuffer<TouchEvent> eventPoolUnConsumed = new RingBuffer<TouchEvent>(MAX_EVENTS);
-    final private RingBuffer<TouchEvent> eventPool = new RingBuffer<TouchEvent>(MAX_EVENTS);
-    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
-    // Internal
-    private View view;
-    private ScaleGestureDetector scaledetector;
-    private boolean scaleInProgress = false;
-    private GestureDetector detector;
-    private int lastX;
-    private int lastY;
-    private final static Logger logger = Logger.getLogger(AndroidInput.class.getName());
-    private boolean isInitialized = false;
-    private RawInputListener listener = null;
-    private static final int[] ANDROID_TO_JME = {
-        0x0, // unknown
-        0x0, // key code soft left
-        0x0, // key code soft right
-        KeyInput.KEY_HOME,
-        KeyInput.KEY_ESCAPE, // key back
-        0x0, // key call
-        0x0, // key endcall
-        KeyInput.KEY_0,
-        KeyInput.KEY_1,
-        KeyInput.KEY_2,
-        KeyInput.KEY_3,
-        KeyInput.KEY_4,
-        KeyInput.KEY_5,
-        KeyInput.KEY_6,
-        KeyInput.KEY_7,
-        KeyInput.KEY_8,
-        KeyInput.KEY_9,
-        KeyInput.KEY_MULTIPLY,
-        0x0, // key pound
-        KeyInput.KEY_UP,
-        KeyInput.KEY_DOWN,
-        KeyInput.KEY_LEFT,
-        KeyInput.KEY_RIGHT,
-        KeyInput.KEY_RETURN, // dpad center
-        0x0, // volume up
-        0x0, // volume down
-        KeyInput.KEY_POWER, // power (?)
-        0x0, // camera
-        0x0, // clear
-        KeyInput.KEY_A,
-        KeyInput.KEY_B,
-        KeyInput.KEY_C,
-        KeyInput.KEY_D,
-        KeyInput.KEY_E,
-        KeyInput.KEY_F,
-        KeyInput.KEY_G,
-        KeyInput.KEY_H,
-        KeyInput.KEY_I,
-        KeyInput.KEY_J,
-        KeyInput.KEY_K,
-        KeyInput.KEY_L,
-        KeyInput.KEY_M,
-        KeyInput.KEY_N,
-        KeyInput.KEY_O,
-        KeyInput.KEY_P,
-        KeyInput.KEY_Q,
-        KeyInput.KEY_R,
-        KeyInput.KEY_S,
-        KeyInput.KEY_T,
-        KeyInput.KEY_U,
-        KeyInput.KEY_V,
-        KeyInput.KEY_W,
-        KeyInput.KEY_X,
-        KeyInput.KEY_Y,
-        KeyInput.KEY_Z,
-        KeyInput.KEY_COMMA,
-        KeyInput.KEY_PERIOD,
-        KeyInput.KEY_LMENU,
-        KeyInput.KEY_RMENU,
-        KeyInput.KEY_LSHIFT,
-        KeyInput.KEY_RSHIFT,
-        //        0x0, // fn
-        //        0x0, // cap (?)
-
-        KeyInput.KEY_TAB,
-        KeyInput.KEY_SPACE,
-        0x0, // sym (?) symbol
-        0x0, // explorer
-        0x0, // envelope
-        KeyInput.KEY_RETURN, // newline/enter
-        KeyInput.KEY_DELETE,
-        KeyInput.KEY_GRAVE,
-        KeyInput.KEY_MINUS,
-        KeyInput.KEY_EQUALS,
-        KeyInput.KEY_LBRACKET,
-        KeyInput.KEY_RBRACKET,
-        KeyInput.KEY_BACKSLASH,
-        KeyInput.KEY_SEMICOLON,
-        KeyInput.KEY_APOSTROPHE,
-        KeyInput.KEY_SLASH,
-        KeyInput.KEY_AT, // at (@)
-        KeyInput.KEY_NUMLOCK, //0x0, // num
-        0x0, //headset hook
-        0x0, //focus
-        KeyInput.KEY_ADD,
-        KeyInput.KEY_LMETA, //menu
-        0x0,//notification
-        0x0,//search
-        0x0,//media play/pause
-        0x0,//media stop
-        0x0,//media next
-        0x0,//media previous
-        0x0,//media rewind
-        0x0,//media fastforward
-        0x0,//mute
-    };
-
-    public AndroidInput() {
-    }
-
-    public void setView(View view) {
-        this.view = view;
-        if (view != null) {
-            detector = new GestureDetector(null, this, null, false);
-            scaledetector = new ScaleGestureDetector(view.getContext(), this);
-            view.setOnTouchListener(this);
-            view.setOnKeyListener(this);
-        }
-    }
-
-    private TouchEvent getNextFreeTouchEvent() {
-        return getNextFreeTouchEvent(false);
-    }
-
-    /**
-     * Fetches a touch event from the reuse pool
-     * @param wait if true waits for a reusable event to get available/released
-     * by an other thread, if false returns a new one if needed.
-     *
-     * @return a usable TouchEvent
-     */
-    private TouchEvent getNextFreeTouchEvent(boolean wait) {
-        TouchEvent evt = null;
-        synchronized (eventPoolUnConsumed) {
-            int size = eventPoolUnConsumed.size();
-            while (size > 0) {
-                evt = eventPoolUnConsumed.pop();
-                if (!evt.isConsumed()) {
-                    eventPoolUnConsumed.push(evt);
-                    evt = null;
-                } else {
-                    break;
-                }
-                size--;
-            }
-        }
-
-        if (evt == null) {
-            if (eventPool.isEmpty() && wait) {
-                logger.warning("eventPool buffer underrun");
-                boolean isEmpty;
-                do {
-                    synchronized (eventPool) {
-                        isEmpty = eventPool.isEmpty();
-                    }
-                    try {
-                        Thread.sleep(50);
-                    } catch (InterruptedException e) {
-                    }
-                } while (isEmpty);
-                synchronized (eventPool) {
-                    evt = eventPool.pop();
-                }
-            } else if (eventPool.isEmpty()) {
-                evt = new TouchEvent();
-                logger.warning("eventPool buffer underrun");
-            } else {
-                synchronized (eventPool) {
-                    evt = eventPool.pop();
-                }
-            }
-        }
-        return evt;
-    }
-
-    /**
-     * onTouch gets called from android thread on touchpad events
-     */
-    public boolean onTouch(View view, MotionEvent event) {
-        if (view != this.view) {
-            return false;
-        }
-        boolean bWasHandled = false;
-        TouchEvent touch;
-        //    System.out.println("native : " + event.getAction());
-        int action = event.getAction() & MotionEvent.ACTION_MASK;
-        int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
-                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-        int pointerId = event.getPointerId(pointerIndex);
-        Vector2f lastPos = lastPositions.get(pointerId);
-
-        // final int historySize = event.getHistorySize();
-        //final int pointerCount = event.getPointerCount();
-        switch (action) {
-            case MotionEvent.ACTION_POINTER_DOWN:
-            case MotionEvent.ACTION_DOWN:
-                touch = getNextFreeTouchEvent();
-                touch.set(Type.DOWN, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
-                touch.setPointerId(pointerId);
-                touch.setTime(event.getEventTime());
-                touch.setPressure(event.getPressure(pointerIndex));
-                processEvent(touch);
-
-                lastPos = new Vector2f(event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex));
-                lastPositions.put(pointerId, lastPos);
-
-                bWasHandled = true;
-                break;
-            case MotionEvent.ACTION_POINTER_UP:
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                touch = getNextFreeTouchEvent();
-                touch.set(Type.UP, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
-                touch.setPointerId(pointerId);
-                touch.setTime(event.getEventTime());
-                touch.setPressure(event.getPressure(pointerIndex));
-                processEvent(touch);
-                lastPositions.remove(pointerId);
-
-                bWasHandled = true;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                // Convert all pointers into events
-                for (int p = 0; p < event.getPointerCount(); p++) {
-                    lastPos = lastPositions.get(event.getPointerId(p));
-                    if (lastPos == null) {
-                        lastPos = new Vector2f(event.getX(p), view.getHeight() - event.getY(p));
-                        lastPositions.put(event.getPointerId(p), lastPos);
-                    }
-
-                    float dX = event.getX(p) - lastPos.x;
-                    float dY = view.getHeight() - event.getY(p) - lastPos.y;
-                    if (dX != 0 || dY != 0) {
-                        touch = getNextFreeTouchEvent();
-                        touch.set(Type.MOVE, event.getX(p), view.getHeight() - event.getY(p), dX, dY);
-                        touch.setPointerId(event.getPointerId(p));
-                        touch.setTime(event.getEventTime());
-                        touch.setPressure(event.getPressure(p));
-                        touch.setScaleSpanInProgress(scaleInProgress);
-                        processEvent(touch);
-                        lastPos.set(event.getX(p), view.getHeight() - event.getY(p));
-                    }
-                }
-                bWasHandled = true;
-                break;
-            case MotionEvent.ACTION_OUTSIDE:
-                break;
-
-        }
-
-        // Try to detect gestures
-        this.detector.onTouchEvent(event);
-        this.scaledetector.onTouchEvent(event);
-
-        return bWasHandled;
-    }
-
-    /**
-     * onKey gets called from android thread on key events
-     */
-    public boolean onKey(View view, int keyCode, KeyEvent event) {
-        if (view != this.view) {
-            return false;
-        }
-
-        if (event.getAction() == KeyEvent.ACTION_DOWN) {
-        TouchEvent evt;
-        evt = getNextFreeTouchEvent();
-        evt.set(TouchEvent.Type.KEY_DOWN);
-        evt.setKeyCode(keyCode);
-        evt.setCharacters(event.getCharacters());
-        evt.setTime(event.getEventTime());
-
-        // Send the event
-        processEvent(evt);
-
-        // Handle all keys ourself except Volume Up/Down
-        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
-            return false;
-        } else {
-            return true;
-        }
-        } else if (event.getAction() == KeyEvent.ACTION_UP) {
-        TouchEvent evt;
-        evt = getNextFreeTouchEvent();
-        evt.set(TouchEvent.Type.KEY_UP);
-        evt.setKeyCode(keyCode);
-        evt.setCharacters(event.getCharacters());
-        evt.setTime(event.getEventTime());
-
-        // Send the event
-        processEvent(evt);
-
-        // Handle all keys ourself except Volume Up/Down
-        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
-            return false;
-        } else {
-            return true;
-        }
-        } else {
-            return false;
-        }
-    }
-
-    public void loadSettings(AppSettings settings) {
-        mouseEventsEnabled = settings.isEmulateMouse();
-        mouseEventsInvertX = settings.isEmulateMouseFlipX();
-        mouseEventsInvertY = settings.isEmulateMouseFlipY();
-    }
-
-    // -----------------------------------------
-    // JME3 Input interface
-    @Override
-    public void initialize() {
-        TouchEvent item;
-        for (int i = 0; i < MAX_EVENTS; i++) {
-            item = new TouchEvent();
-            eventPool.push(item);
-        }
-        isInitialized = true;
-    }
-
-    @Override
-    public void destroy() {
-        isInitialized = false;
-
-        // Clean up queues
-        while (!eventPool.isEmpty()) {
-            eventPool.pop();
-        }
-        while (!eventQueue.isEmpty()) {
-            eventQueue.pop();
-        }
-
-
-        this.view = null;
-    }
-
-    @Override
-    public boolean isInitialized() {
-        return isInitialized;
-    }
-
-    @Override
-    public void setInputListener(RawInputListener listener) {
-        this.listener = listener;
-    }
-
-    @Override
-    public long getInputTimeNanos() {
-        return System.nanoTime();
-    }
-    // -----------------------------------------
-
-    private void processEvent(TouchEvent event) {
-        synchronized (eventQueue) {
-            //Discarding events when the ring buffer is full to avoid buffer overflow.
-            if(eventQueue.size()< MAX_EVENTS){
-                eventQueue.push(event);
-            }
-
-        }
-    }
-
-    //  ---------------  INSIDE GLThread  ---------------
-    @Override
-    public void update() {
-        generateEvents();
-    }
-
-    private void generateEvents() {
-        if (listener != null) {
-            TouchEvent event;
-            MouseButtonEvent btn;
-            MouseMotionEvent mot;
-            int newX;
-            int newY;
-
-            while (!eventQueue.isEmpty()) {
-                synchronized (eventQueue) {
-                    event = eventQueue.pop();
-                }
-                if (event != null) {
-                    listener.onTouchEvent(event);
-
-                    if (mouseEventsEnabled) {
-                        if (mouseEventsInvertX) {
-                            newX = view.getWidth() - (int) event.getX();
-                        } else {
-                            newX = (int) event.getX();
-                        }
-
-                        if (mouseEventsInvertY) {
-                            newY = view.getHeight() - (int) event.getY();
-                        } else {
-                            newY = (int) event.getY();
-                        }
-
-                        switch (event.getType()) {
-                            case DOWN:
-                                // Handle mouse down event
-                                btn = new MouseButtonEvent(0, true, newX, newY);
-                                btn.setTime(event.getTime());
-                                listener.onMouseButtonEvent(btn);
-                                // Store current pos
-                                lastX = -1;
-                                lastY = -1;
-                                break;
-
-                            case UP:
-                                // Handle mouse up event
-                                btn = new MouseButtonEvent(0, false, newX, newY);
-                                btn.setTime(event.getTime());
-                                listener.onMouseButtonEvent(btn);
-                                // Store current pos
-                                lastX = -1;
-                                lastY = -1;
-                                break;
-
-                            case SCALE_MOVE:
-                                if (lastX != -1 && lastY != -1) {
-                                    newX = lastX;
-                                    newY = lastY;
-                                }
-                                int wheel = (int) (event.getScaleSpan() / 4f); // scale to match mouse wheel
-                                int dwheel = (int) (event.getDeltaScaleSpan() / 4f); // scale to match mouse wheel
-                                mot = new MouseMotionEvent(newX, newX, 0, 0, wheel, dwheel);
-                                mot.setTime(event.getTime());
-                                listener.onMouseMotionEvent(mot);
-                                lastX = newX;
-                                lastY = newY;
-
-                                break;
-
-                            case MOVE:
-                                if (event.isScaleSpanInProgress()) {
-                                    break;
-                                }
-
-                                int dx;
-                                int dy;
-                                if (lastX != -1) {
-                                    dx = newX - lastX;
-                                    dy = newY - lastY;
-                                } else {
-                                    dx = 0;
-                                    dy = 0;
-                                }
-
-                                mot = new MouseMotionEvent(newX, newY, dx, dy, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
-                                mot.setTime(event.getTime());
-                                listener.onMouseMotionEvent(mot);
-                                lastX = newX;
-                                lastY = newY;
-
-                                break;
-                        }
-                    }
-                }
-
-                if (event.isConsumed() == false) {
-                    synchronized (eventPoolUnConsumed) {
-                        eventPoolUnConsumed.push(event);
-                    }
-
-                } else {
-                    synchronized (eventPool) {
-                        eventPool.push(event);
-                    }
-                }
-            }
-
-        }
-    }
-    //  --------------- ENDOF INSIDE GLThread  ---------------
-
-    // --------------- Gesture detected callback events  ---------------
-    public boolean onDown(MotionEvent event) {
-        return false;
-    }
-
-    public void onLongPress(MotionEvent event) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.LONGPRESSED, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(event.getEventTime());
-        processEvent(touch);
-    }
-
-    public boolean onFling(MotionEvent event, MotionEvent event2, float vx, float vy) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.FLING, event.getX(), view.getHeight() - event.getY(), vx, vy);
-        touch.setPointerId(0);
-        touch.setTime(event.getEventTime());
-        processEvent(touch);
-
-        return true;
-    }
-
-    public boolean onSingleTapConfirmed(MotionEvent event) {
-        //Nothing to do here the tap has already been detected.
-        return false;
-    }
-
-    public boolean onDoubleTap(MotionEvent event) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.DOUBLETAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(event.getEventTime());
-        processEvent(touch);
-        return true;
-    }
-
-    public boolean onDoubleTapEvent(MotionEvent event) {
-        return false;
-    }
-
-    public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
-        scaleInProgress = true;
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.SCALE_START, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(scaleGestureDetector.getEventTime());
-        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
-        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
-        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touch.setScaleSpanInProgress(scaleInProgress);
-        processEvent(touch);
-        //    System.out.println("scaleBegin");
-
-        return true;
-    }
-
-    public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.SCALE_MOVE, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(scaleGestureDetector.getEventTime());
-        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
-        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
-        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touch.setScaleSpanInProgress(scaleInProgress);
-        processEvent(touch);
-        //   System.out.println("scale");
-
-        return false;
-    }
-
-    public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
-        scaleInProgress = false;
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.SCALE_END, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(scaleGestureDetector.getEventTime());
-        touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
-        touch.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan());
-        touch.setScaleFactor(scaleGestureDetector.getScaleFactor());
-        touch.setScaleSpanInProgress(scaleInProgress);
-        processEvent(touch);
-    }
-
-    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.SCROLL, e1.getX(), view.getHeight() - e1.getY(), distanceX, distanceY * (-1));
-        touch.setPointerId(0);
-        touch.setTime(e1.getEventTime());
-        processEvent(touch);
-        //System.out.println("scroll " + e1.getPointerCount());
-        return false;
-    }
-
-    public void onShowPress(MotionEvent event) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.SHOWPRESS, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(event.getEventTime());
-        processEvent(touch);
-    }
-
-    public boolean onSingleTapUp(MotionEvent event) {
-        TouchEvent touch = getNextFreeTouchEvent();
-        touch.set(Type.TAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
-        touch.setPointerId(0);
-        touch.setTime(event.getEventTime());
-        processEvent(touch);
-        return true;
-    }
-
-    @Override
-    public void setSimulateKeyboard(boolean simulate) {
-        keyboardEventsEnabled = simulate;
-    }
-
-    @Override
-    public void setOmitHistoricEvents(boolean dontSendHistory) {
-        this.dontSendHistory = dontSendHistory;
-    }
-
-    /**
-     * @deprecated Use {@link #getSimulateMouse()};
-     */
-    @Deprecated
-    public boolean isMouseEventsEnabled() {
-        return mouseEventsEnabled;
-    }
-
-    @Deprecated
-    public void setMouseEventsEnabled(boolean mouseEventsEnabled) {
-        this.mouseEventsEnabled = mouseEventsEnabled;
-    }
-
-    public boolean isMouseEventsInvertY() {
-        return mouseEventsInvertY;
-    }
-
-    public void setMouseEventsInvertY(boolean mouseEventsInvertY) {
-        this.mouseEventsInvertY = mouseEventsInvertY;
-    }
-
-    public boolean isMouseEventsInvertX() {
-        return mouseEventsInvertX;
-    }
-
-    public void setMouseEventsInvertX(boolean mouseEventsInvertX) {
-        this.mouseEventsInvertX = mouseEventsInvertX;
-    }
-
-    public void setSimulateMouse(boolean simulate) {
-        mouseEventsEnabled = simulate;
-    }
-
-    public boolean getSimulateMouse() {
-        return isSimulateMouse();
-    }
-
-    public boolean isSimulateMouse() {
-        return mouseEventsEnabled;
-    }
-
-    public boolean isSimulateKeyboard() {
-        return keyboardEventsEnabled;
-    }
-
-}

+ 147 - 172
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java

@@ -33,231 +33,206 @@
 package com.jme3.input.android;
 
 import android.opengl.GLSurfaceView;
-import android.os.Build;
+import android.view.GestureDetector;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
 import android.view.View;
-import com.jme3.input.RawInputListener;
+import com.jme3.input.JoyInput;
 import com.jme3.input.TouchInput;
-import com.jme3.input.event.InputEvent;
-import com.jme3.input.event.KeyInputEvent;
-import com.jme3.input.event.MouseButtonEvent;
-import com.jme3.input.event.MouseMotionEvent;
-import com.jme3.input.event.TouchEvent;
 import com.jme3.system.AppSettings;
-import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
  * <code>AndroidInput</code> is the main class that connects the Android system
- * inputs to jME. It serves as the manager that gathers inputs from the various
- * Android input methods and provides them to jME's <code>InputManager</code>.
+ * inputs to jME. It receives the inputs from the Android View and passes them
+ * to the appropriate classes based on the source of the input.</br>
+ * This class is to be extended when new functionality is released in Android.
  *
  * @author iwgeric
  */
-public class AndroidInputHandler implements TouchInput {
-    private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
-
-    // Custom settings
-    private boolean mouseEventsEnabled = true;
-    private boolean mouseEventsInvertX = false;
-    private boolean mouseEventsInvertY = false;
-    private boolean keyboardEventsEnabled = false;
-    private boolean dontSendHistory = false;
+public class AndroidInputHandler implements View.OnTouchListener,
+                                            View.OnKeyListener {
 
+    private static final Logger logger = Logger.getLogger(AndroidInputHandler.class.getName());
 
-    // Internal
-    private GLSurfaceView view;
-    private AndroidTouchHandler touchHandler;
-    private AndroidKeyHandler keyHandler;
-    private AndroidGestureHandler gestureHandler;
-    private boolean initialized = false;
-    private RawInputListener listener = null;
-    private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<InputEvent>();
-    private final static int MAX_TOUCH_EVENTS = 1024;
-    private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS);
-    private float scaleX = 1f;
-    private float scaleY = 1f;
+    protected GLSurfaceView view;
+    protected AndroidTouchInput touchInput;
+    protected AndroidJoyInput joyInput;
 
 
     public AndroidInputHandler() {
-        int buildVersion = Build.VERSION.SDK_INT;
-        logger.log(Level.INFO, "Android Build Version: {0}", buildVersion);
-        if (buildVersion >= 14) {
-            // add support for onHover and GenericMotionEvent (ie. gamepads)
-            gestureHandler = new AndroidGestureHandler(this);
-            touchHandler = new AndroidTouchHandler14(this, gestureHandler);
-            keyHandler = new AndroidKeyHandler(this);
-        } else if (buildVersion >= 8){
-            gestureHandler = new AndroidGestureHandler(this);
-            touchHandler = new AndroidTouchHandler(this, gestureHandler);
-            keyHandler = new AndroidKeyHandler(this);
-        }
-    }
-
-    public AndroidInputHandler(AndroidTouchHandler touchInput,
-            AndroidKeyHandler keyInput, AndroidGestureHandler gestureHandler) {
-        this.touchHandler = touchInput;
-        this.keyHandler = keyInput;
-        this.gestureHandler = gestureHandler;
+        touchInput = new AndroidTouchInput(this);
+        joyInput = new AndroidJoyInput(this);
     }
 
     public void setView(View view) {
-        if (touchHandler != null) {
-            touchHandler.setView(view);
+        if (this.view != null && view != null && this.view.equals(view)) {
+            return;
         }
-        if (keyHandler != null) {
-            keyHandler.setView(view);
-        }
-        if (gestureHandler != null) {
-            gestureHandler.setView(view);
+
+        if (this.view != null) {
+            removeListeners(this.view);
         }
+
         this.view = (GLSurfaceView)view;
-    }
 
-    public View getView() {
-        return view;
-    }
+        if (this.view != null) {
+            addListeners(this.view);
+        }
 
-    public float invertX(float origX) {
-        return getJmeX(view.getWidth()) - origX;
+        joyInput.setView((GLSurfaceView)view);
     }
 
-    public float invertY(float origY) {
-        return getJmeY(view.getHeight()) - origY;
+    public View getView() {
+        return view;
     }
 
-    public float getJmeX(float origX) {
-        return origX * scaleX;
+    protected void removeListeners(GLSurfaceView view) {
+        view.setOnTouchListener(null);
+        view.setOnKeyListener(null);
+        touchInput.setGestureDetector(null);
+        touchInput.setScaleDetector(null);
     }
 
-    public float getJmeY(float origY) {
-        return origY * scaleY;
+    protected void addListeners(GLSurfaceView view) {
+        view.setOnTouchListener(this);
+        view.setOnKeyListener(this);
+        AndroidGestureProcessor gestureHandler = new AndroidGestureProcessor(touchInput);
+        touchInput.setGestureDetector(new GestureDetector(
+                view.getContext(), gestureHandler));
+        touchInput.setScaleDetector(new ScaleGestureDetector(
+                view.getContext(), gestureHandler));
     }
 
     public void loadSettings(AppSettings settings) {
-        keyboardEventsEnabled = settings.isEmulateKeyboard();
-        mouseEventsEnabled = settings.isEmulateMouse();
-        mouseEventsInvertX = settings.isEmulateMouseFlipX();
-        mouseEventsInvertY = settings.isEmulateMouseFlipY();
-
-        // view width and height are 0 until the view is displayed on the screen
-        if (view.getWidth() != 0 && view.getHeight() != 0) {
-            scaleX = (float)settings.getWidth() / (float)view.getWidth();
-            scaleY = (float)settings.getHeight() / (float)view.getHeight();
-        }
-        logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
-                new Object[]{scaleX, scaleY});
+        touchInput.loadSettings(settings);
+    }
+
+    public TouchInput getTouchInput() {
+        return touchInput;
+    }
+
+    public JoyInput getJoyInput() {
+        return joyInput;
+    }
+
+    /*
+     *  Android input events include the source from which the input came from.
+     *  We must look at the source of the input event to determine which type
+     *  of jME input it belongs to.
+     *  If the input is from a gamepad or joystick source, the event is sent
+     *  to the JoyInput class to convert the event into jME joystick events.
+     *  </br>
+     *  If the input is from a touchscreen source, the event is sent to the
+     *  TouchProcessor to convert the event into touch events.
+     *  The TouchProcessor also converts the events into Mouse and Key events
+     *  if AppSettings is set to simulate Mouse or Keyboard events.
+     *
+     *  Android reports the source as a bitmask as shown below.</br>
+     *
+     *  InputDevice Sources
+     *     0000 0000 0000 0000 0000 0000 0000 0000 - 32 bit bitmask
+     *
+     *     0000 0000 0000 0000 0000 0000 1111 1111 - SOURCE_CLASS_MASK       (0x000000ff)
+     *     0000 0000 0000 0000 0000 0000 0000 0000 - SOURCE_CLASS_NONE       (0x00000000)
+     *     0000 0000 0000 0000 0000 0000 0000 0001 - SOURCE_CLASS_BUTTON     (0x00000001)
+     *     0000 0000 0000 0000 0000 0000 0000 0010 - SOURCE_CLASS_POINTER    (0x00000002)
+     *     0000 0000 0000 0000 0000 0000 0000 0100 - SOURCE_CLASS_TRACKBALL  (0x00000004)
+     *     0000 0000 0000 0000 0000 0000 0000 1000 - SOURCE_CLASS_POSITION   (0x00000008)
+     *     0000 0000 0000 0000 0000 0000 0001 0000 - SOURCE_CLASS_JOYSTICK   (0x00000010)
+     *
+     *     1111 1111 1111 1111 1111 1111 0000 0000 - Source_Any              (0xffffff00)
+     *     0000 0000 0000 0000 0000 0000 0000 0000 - SOURCE_UNKNOWN          (0x00000000)
+     *     0000 0000 0000 0000 0000 0001 0000 0001 - SOURCE_KEYBOARD         (0x00000101)
+     *     0000 0000 0000 0000 0000 0010 0000 0001 - SOURCE_DPAD             (0x00000201)
+     *     0000 0000 0000 0000 0000 0100 0000 0001 - SOURCE_GAMEPAD          (0x00000401)
+     *     0000 0000 0000 0000 0001 0000 0000 0010 - SOURCE_TOUCHSCREEN      (0x00001002)
+     *     0000 0000 0000 0000 0010 0000 0000 0010 - SOURCE_MOUSE            (0x00002002)
+     *     0000 0000 0000 0000 0100 0000 0000 0010 - SOURCE_STYLUS           (0x00004002)
+     *     0000 0000 0000 0001 0000 0000 0000 0100 - SOURCE_TRACKBALL        (0x00010004)
+     *     0000 0000 0001 0000 0000 0000 0000 1000 - SOURCE_TOUCHPAD         (0x00100008)
+     *     0000 0000 0010 0000 0000 0000 0000 0000 - SOURCE_TOUCH_NAVIGATION (0x00200000)
+     *     0000 0001 0000 0000 0000 0000 0001 0000 - SOURCE_JOYSTICK         (0x01000010)
+     *     0000 0010 0000 0000 0000 0000 0000 0001 - SOURCE_HDMI             (0x02000001)
+     *
+     * Example values reported by Android for Source
+     * 4,098 = 0x00001002 =
+     *     0000 0000 0000 0000 0001 0000 0000 0010 - SOURCE_CLASS_POINTER
+     *                                               SOURCE_TOUCHSCREEN
+     * 1,281 = 0x00000501 =
+     *     0000 0000 0000 0000 0000 0101 0000 0001 - SOURCE_CLASS_BUTTON
+     *                                               SOURCE_KEYBOARD
+     *                                               SOURCE_GAMEPAD
+     * 16,777,232 = 0x01000010 =
+     *     0000 0001 0000 0000 0000 0000 0001 0000 - SOURCE_CLASS_JOYSTICK
+     *                                               SOURCE_JOYSTICK
+     *
+     * 16,778,513 = 0x01000511 =
+     *     0000 0001 0000 0000 0000 0101 0001 0001 - SOURCE_CLASS_BUTTON
+     *                                               SOURCE_CLASS_JOYSTICK
+     *                                               SOURCE_GAMEPAD
+     *                                               SOURCE_KEYBOARD
+     *                                               SOURCE_JOYSTICK
+     *
+     * 257 = 0x00000101 =
+     *     0000 0000 0000 0000 0000 0001 0000 0001 - SOURCE_CLASS_BUTTON
+     *                                               SOURCE_KEYBOARD
+     *
+     *
+     *
+     */
 
-    }
 
-        // -----------------------------------------
-    // JME3 Input interface
     @Override
-    public void initialize() {
-        touchEventPool.initialize();
-        if (touchHandler != null) {
-            touchHandler.initialize();
-        }
-        if (keyHandler != null) {
-            keyHandler.initialize();
-        }
-        if (gestureHandler != null) {
-            gestureHandler.initialize();
+    public boolean onTouch(View view, MotionEvent event) {
+        if (view != getView()) {
+            return false;
         }
 
-        initialized = true;
-    }
+        boolean consumed = false;
 
-    @Override
-    public void destroy() {
-        initialized = false;
+        int source = event.getSource();
+//        logger.log(Level.INFO, "onTouch source: {0}", source);
 
-        touchEventPool.destroy();
-        if (touchHandler != null) {
-            touchHandler.destroy();
-        }
-        if (keyHandler != null) {
-            keyHandler.destroy();
-        }
-        if (gestureHandler != null) {
-            gestureHandler.destroy();
-        }
-
-        setView(null);
-    }
-
-    @Override
-    public boolean isInitialized() {
-        return initialized;
-    }
-
-    @Override
-    public void setInputListener(RawInputListener listener) {
-        this.listener = listener;
-    }
-
-    @Override
-    public long getInputTimeNanos() {
-        return System.nanoTime();
-    }
+        boolean isTouch = ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN);
+//        logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}",
+//                new Object[]{source, isTouch});
 
-    public void update() {
-        if (listener != null) {
-            InputEvent inputEvent;
-
-            while ((inputEvent = inputEventQueue.poll()) != null) {
-                if (inputEvent instanceof TouchEvent) {
-                    listener.onTouchEvent((TouchEvent)inputEvent);
-                } else if (inputEvent instanceof MouseButtonEvent) {
-                    listener.onMouseButtonEvent((MouseButtonEvent)inputEvent);
-                } else if (inputEvent instanceof MouseMotionEvent) {
-                    listener.onMouseMotionEvent((MouseMotionEvent)inputEvent);
-                } else if (inputEvent instanceof KeyInputEvent) {
-                    listener.onKeyEvent((KeyInputEvent)inputEvent);
-                }
-            }
+        if (isTouch && touchInput != null) {
+            // send the event to the touch processor
+            consumed = touchInput.onTouch(event);
         }
-    }
 
-    // -----------------------------------------
+        return consumed;
 
-    public TouchEvent getFreeTouchEvent() {
-            return touchEventPool.getNextFreeEvent();
     }
 
-    public void addEvent(InputEvent event) {
-        inputEventQueue.add(event);
-        if (event instanceof TouchEvent) {
-            touchEventPool.storeEvent((TouchEvent)event);
+    @Override
+    public boolean onKey(View view, int keyCode, KeyEvent event) {
+        if (view != getView()) {
+            return false;
         }
-    }
 
-    public void setSimulateMouse(boolean simulate) {
-        this.mouseEventsEnabled = simulate;
-    }
+        boolean consumed = false;
 
-    public boolean isSimulateMouse() {
-        return mouseEventsEnabled;
-    }
+        int source = event.getSource();
+//        logger.log(Level.INFO, "onKey source: {0}", source);
 
-    public boolean isMouseEventsInvertX() {
-        return mouseEventsInvertX;
-    }
+        boolean isTouch =
+                ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) ||
+                ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD);
+//        logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
+//                new Object[]{source, isTouch});
 
-    public boolean isMouseEventsInvertY() {
-        return mouseEventsInvertY;
-    }
-
-    public void setSimulateKeyboard(boolean simulate) {
-        this.keyboardEventsEnabled = simulate;
-    }
+        if (touchInput != null) {
+            consumed = touchInput.onKey(event);
+        }
 
-    public boolean isSimulateKeyboard() {
-        return keyboardEventsEnabled;
-    }
+        return consumed;
 
-    public void setOmitHistoricEvents(boolean dontSendHistory) {
-        this.dontSendHistory = dontSendHistory;
     }
 
 }

+ 159 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.opengl.GLSurfaceView;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidInputHandler14</code> extends <code>AndroidInputHandler</code> to
+ * add the onHover and onGenericMotion events that where added in Android rev 14 (Android 4.0).</br>
+ * The onGenericMotion events are the main interface to Joystick axes.  They
+ * were actually released in Android rev 12.
+ *
+ * @author iwgeric
+ */
+public class AndroidInputHandler14 extends AndroidInputHandler implements View.OnHoverListener,
+                                                                            View.OnGenericMotionListener {
+
+    private static final Logger logger = Logger.getLogger(AndroidInputHandler14.class.getName());
+
+    public AndroidInputHandler14() {
+        touchInput = new AndroidTouchInput14(this);
+        joyInput = new AndroidJoyInput14(this);
+    }
+
+    @Override
+    protected void removeListeners(GLSurfaceView view) {
+        super.removeListeners(view);
+        view.setOnHoverListener(null);
+        view.setOnGenericMotionListener(null);
+    }
+
+    @Override
+    protected void addListeners(GLSurfaceView view) {
+        super.addListeners(view);
+        view.setOnHoverListener(this);
+        view.setOnGenericMotionListener(this);
+    }
+
+    @Override
+    public boolean onHover(View view, MotionEvent event) {
+        if (view != getView()) {
+            return false;
+        }
+
+        boolean consumed = false;
+
+        int source = event.getSource();
+//        logger.log(Level.INFO, "onTouch source: {0}", source);
+
+        boolean isTouch = ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN);
+//        logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}",
+//                new Object[]{source, isTouch});
+
+        if (isTouch && touchInput != null) {
+            // send the event to the touch processor
+            consumed = ((AndroidTouchInput14)touchInput).onHover(event);
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public boolean onGenericMotion(View view, MotionEvent event) {
+        if (view != getView()) {
+            return false;
+        }
+
+        boolean consumed = false;
+
+        int source = event.getSource();
+//        logger.log(Level.INFO, "onGenericMotion source: {0}", source);
+
+        boolean isJoystick =
+                ((source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+                ((source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
+
+        if (isJoystick && joyInput != null) {
+//            logger.log(Level.INFO, "onGenericMotion source: {0}, isJoystick: {1}",
+//                    new Object[]{source, isJoystick});
+            // send the event to the touch processor
+            consumed = consumed || ((AndroidJoyInput14)joyInput).onGenericMotion(event);
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public boolean onKey(View view, int keyCode, KeyEvent event) {
+        if (view != getView()) {
+            return false;
+        }
+
+        boolean consumed = false;
+
+//        logger.log(Level.INFO, "onKey keyCode: {0}, action: {1}, event: {2}",
+//                new Object[]{KeyEvent.keyCodeToString(keyCode), event.getAction(), event});
+        int source = event.getSource();
+//        logger.log(Level.INFO, "onKey source: {0}", source);
+
+        boolean isTouch =
+                ((source & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) ||
+                ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD);
+        boolean isJoystick =
+                ((source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+                ((source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
+
+        if (isTouch && touchInput != null) {
+//            logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
+//                    new Object[]{source, isTouch});
+            consumed = touchInput.onKey(event);
+        }
+        if (isJoystick && joyInput != null) {
+//            logger.log(Level.INFO, "onKey source: {0}, isJoystick: {1}",
+//                    new Object[]{source, isJoystick});
+            // use inclusive OR to make sure the onKey method is called.
+            consumed = consumed | ((AndroidJoyInput14)joyInput).onKey(event);
+        }
+
+        return consumed;
+
+    }
+
+}

+ 24 - 43
jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInputHandler.java → jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java

@@ -33,9 +33,7 @@ package com.jme3.input.android;
 
 import android.content.Context;
 import android.opengl.GLSurfaceView;
-import android.os.Build;
 import android.os.Vibrator;
-import android.view.View;
 import com.jme3.input.InputManager;
 import com.jme3.input.JoyInput;
 import com.jme3.input.Joystick;
@@ -79,15 +77,16 @@ import java.util.logging.Logger;
  *
  * @author iwgeric
  */
-public class AndroidJoyInputHandler implements JoyInput {
-    private static final Logger logger = Logger.getLogger(AndroidJoyInputHandler.class.getName());
+public class AndroidJoyInput implements JoyInput {
+    private static final Logger logger = Logger.getLogger(AndroidJoyInput.class.getName());
+    public static boolean disableSensors = false;
 
-    private List<Joystick> joystickList = new ArrayList<Joystick>();
+    protected AndroidInputHandler inputHandler;
+    protected List<Joystick> joystickList = new ArrayList<Joystick>();
 //    private boolean dontSendHistory = false;
 
 
     // Internal
-    private GLSurfaceView view;
     private boolean initialized = false;
     private RawInputListener listener = null;
     private ConcurrentLinkedQueue<InputEvent> eventQueue = new ConcurrentLinkedQueue<InputEvent>();
@@ -96,34 +95,29 @@ public class AndroidJoyInputHandler implements JoyInput {
     private boolean vibratorActive = false;
     private long maxRumbleTime = 250;  // 250ms
 
-    public AndroidJoyInputHandler() {
-        int buildVersion = Build.VERSION.SDK_INT;
-        logger.log(Level.INFO, "Android Build Version: {0}", buildVersion);
-//        if (buildVersion >= 14) {
-//            touchHandler = new AndroidTouchHandler14(this);
-//        } else if (buildVersion >= 8){
-//            touchHandler = new AndroidTouchHandler(this);
-//        }
+    public AndroidJoyInput(AndroidInputHandler inputHandler) {
+        this.inputHandler = inputHandler;
         sensorJoyInput = new AndroidSensorJoyInput(this);
     }
 
     public void setView(GLSurfaceView view) {
-//        if (touchHandler != null) {
-//            touchHandler.setView(view);
-//        }
+        if (view == null) {
+            vibrator = null;
+        } else {
+            // Get instance of Vibrator from current Context
+            vibrator = (Vibrator) view.getContext().getSystemService(Context.VIBRATOR_SERVICE);
+            if (vibrator == null) {
+                logger.log(Level.FINE, "Vibrator Service not found.");
+            }
+        }
+
         if (sensorJoyInput != null) {
             sensorJoyInput.setView(view);
         }
-        this.view = (GLSurfaceView)view;
-
-    }
-
-    public View getView() {
-        return view;
     }
 
     public void loadSettings(AppSettings settings) {
-//        sensorEventsEnabled = settings.useSensors();
+
     }
 
     public void addEvent(InputEvent event) {
@@ -155,20 +149,8 @@ public class AndroidJoyInputHandler implements JoyInput {
 
     }
 
-
-
-
-
     @Override
     public void initialize() {
-//        if (sensorJoyInput != null) {
-//            sensorJoyInput.initialize();
-//        }
-        // Get instance of Vibrator from current Context
-        vibrator = (Vibrator) view.getContext().getSystemService(Context.VIBRATOR_SERVICE);
-        if (vibrator == null) {
-            logger.log(Level.FINE, "Vibrator Service not found.");
-        }
         initialized = true;
     }
 
@@ -211,8 +193,8 @@ public class AndroidJoyInputHandler implements JoyInput {
             };
             final int rumbleRepeatFrom = 0; // index into rumble pattern to repeat from
 
-            logger.log(Level.FINE, "Rumble amount: {0}, rumbleOnDur: {1}, rumbleOffDur: {2}",
-                    new Object[]{amount, rumbleOnDur, rumbleOffDur});
+//            logger.log(Level.FINE, "Rumble amount: {0}, rumbleOnDur: {1}, rumbleOffDur: {2}",
+//                    new Object[]{amount, rumbleOnDur, rumbleOffDur});
 
             if (rumbleOnDur > 0) {
                 vibrator.vibrate(rumblePattern, rumbleRepeatFrom);
@@ -226,9 +208,10 @@ public class AndroidJoyInputHandler implements JoyInput {
 
     @Override
     public Joystick[] loadJoysticks(InputManager inputManager) {
-        joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager));
-
-
+        logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName());
+        if (!disableSensors) {
+            joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager));
+        }
         return joystickList.toArray( new Joystick[joystickList.size()] );
     }
 
@@ -252,6 +235,4 @@ public class AndroidJoyInputHandler implements JoyInput {
 
     }
 
-
-
 }

+ 108 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2009-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.input.android;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import com.jme3.input.InputManager;
+import com.jme3.input.Joystick;
+import java.util.logging.Logger;
+
+/**
+ * <code>AndroidJoyInput14</code> extends <code>AndroidJoyInput</code>
+ * to include support for physical joysticks/gamepads.</br>
+ *
+ * @author iwgeric
+ */
+public class AndroidJoyInput14 extends AndroidJoyInput {
+    private static final Logger logger = Logger.getLogger(AndroidJoyInput14.class.getName());
+
+    private AndroidJoystickJoyInput14 joystickJoyInput;
+
+    public AndroidJoyInput14(AndroidInputHandler inputHandler) {
+        super(inputHandler);
+        joystickJoyInput = new AndroidJoystickJoyInput14(this);
+    }
+
+    /**
+     * Pauses the joystick device listeners to save battery life if they are not needed.
+     * Used to pause when the activity pauses
+     */
+    @Override
+    public void pauseJoysticks() {
+        super.pauseJoysticks();
+
+        if (joystickJoyInput != null) {
+            joystickJoyInput.pauseJoysticks();
+        }
+    }
+
+    /**
+     * Resumes the joystick device listeners.
+     * Used to resume when the activity comes to the top of the stack
+     */
+    @Override
+    public void resumeJoysticks() {
+        super.resumeJoysticks();
+        if (joystickJoyInput != null) {
+            joystickJoyInput.resumeJoysticks();
+        }
+
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        if (joystickJoyInput != null) {
+            joystickJoyInput.destroy();
+        }
+    }
+
+    @Override
+    public Joystick[] loadJoysticks(InputManager inputManager) {
+        // load the simulated joystick for device orientation
+        super.loadJoysticks(inputManager);
+        // load physical gamepads/joysticks
+        joystickList.addAll(joystickJoyInput.loadJoysticks(joystickList.size(), inputManager));
+        // return the list of joysticks back to InputManager
+        return joystickList.toArray( new Joystick[joystickList.size()] );
+    }
+
+    public boolean onGenericMotion(MotionEvent event) {
+        return joystickJoyInput.onGenericMotion(event);
+    }
+
+    public boolean onKey(KeyEvent event) {
+        return joystickJoyInput.onKey(event);
+    }
+
+}

+ 416 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java

@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2009-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.input.android;
+
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import com.jme3.input.AbstractJoystick;
+import com.jme3.input.DefaultJoystickAxis;
+import com.jme3.input.DefaultJoystickButton;
+import com.jme3.input.InputManager;
+import com.jme3.input.JoyInput;
+import com.jme3.input.Joystick;
+import com.jme3.input.JoystickAxis;
+import com.jme3.input.JoystickButton;
+import com.jme3.input.JoystickCompatibilityMappings;
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.input.event.JoyButtonEvent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Main class that creates and manages Android inputs for physical gamepads/joysticks.
+ *
+ * @author iwgeric
+ */
+public class AndroidJoystickJoyInput14 {
+    private static final Logger logger = Logger.getLogger(AndroidJoystickJoyInput14.class.getName());
+
+    private boolean loaded = false;
+    private AndroidJoyInput joyInput;
+    private Map<Integer, AndroidJoystick> joystickIndex = new HashMap<Integer, AndroidJoystick>();
+
+    private static int[] AndroidGamepadButtons = {
+            // Dpad buttons
+            KeyEvent.KEYCODE_DPAD_UP,        KeyEvent.KEYCODE_DPAD_DOWN,
+            KeyEvent.KEYCODE_DPAD_LEFT,      KeyEvent.KEYCODE_DPAD_RIGHT,
+            KeyEvent.KEYCODE_DPAD_CENTER,
+
+            // pressing joystick down
+            KeyEvent.KEYCODE_BUTTON_THUMBL,  KeyEvent.KEYCODE_BUTTON_THUMBR,
+
+            // buttons
+            KeyEvent.KEYCODE_BUTTON_A,       KeyEvent.KEYCODE_BUTTON_B,
+            KeyEvent.KEYCODE_BUTTON_X,       KeyEvent.KEYCODE_BUTTON_Y,
+
+            // buttons on back of device
+            KeyEvent.KEYCODE_BUTTON_L1,      KeyEvent.KEYCODE_BUTTON_R1,
+            KeyEvent.KEYCODE_BUTTON_L2,      KeyEvent.KEYCODE_BUTTON_R2,
+
+            // start / select buttons
+            KeyEvent.KEYCODE_BUTTON_START,   KeyEvent.KEYCODE_BUTTON_SELECT,
+            KeyEvent.KEYCODE_BUTTON_MODE,
+
+    };
+
+    public AndroidJoystickJoyInput14(AndroidJoyInput joyInput) {
+        this.joyInput = joyInput;
+    }
+
+
+    public void pauseJoysticks() {
+
+    }
+
+    public void resumeJoysticks() {
+
+    }
+
+    public void destroy() {
+
+    }
+
+    public List<Joystick> loadJoysticks(int joyId, InputManager inputManager) {
+        logger.log(Level.INFO, "loading Joystick devices");
+        ArrayList<Joystick> joysticks = new ArrayList<Joystick>();
+        joysticks.clear();
+        joystickIndex.clear();
+
+        ArrayList gameControllerDeviceIds = new ArrayList();
+        int[] deviceIds = InputDevice.getDeviceIds();
+        for (int deviceId : deviceIds) {
+            InputDevice dev = InputDevice.getDevice(deviceId);
+            int sources = dev.getSources();
+            logger.log(Level.FINE, "deviceId[{0}] sources: {1}", new Object[]{deviceId, sources});
+
+            // Verify that the device has gamepad buttons, control sticks, or both.
+            if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+                    ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
+                // This device is a game controller. Store its device ID.
+                if (!gameControllerDeviceIds.contains(deviceId)) {
+                    gameControllerDeviceIds.add(deviceId);
+                    logger.log(Level.FINE, "Attempting to create joystick for device: {0}", dev);
+                    // Create an AndroidJoystick and store the InputDevice so we
+                    // can later correspond the input from the InputDevice to the
+                    // appropriate jME Joystick event
+                    AndroidJoystick joystick = new AndroidJoystick(inputManager,
+                                                                joyInput,
+                                                                dev,
+                                                                joyId+joysticks.size(),
+                                                                dev.getName());
+                    joystickIndex.put(deviceId, joystick);
+                    joysticks.add(joystick);
+
+                    // Each analog input is reported as a MotionRange
+                    // The axis number corresponds to the type of axis
+                    // The AndroidJoystick.addAxis(MotionRange) converts the axis
+                    // type reported by Android into the jME Joystick axis
+                    List<MotionRange> motionRanges = dev.getMotionRanges();
+                    for (MotionRange motionRange: motionRanges) {
+                        logger.log(Level.INFO, "motion range: {0}", motionRange.toString());
+                        logger.log(Level.INFO, "axis: {0}", motionRange.getAxis());
+                        JoystickAxis axis = joystick.addAxis(motionRange);
+                        logger.log(Level.INFO, "added axis: {0}", axis);
+                    }
+
+                    // InputDevice has a method for determining if a keyCode is
+                    // supported (InputDevice  public boolean[] hasKeys (int... keys)).
+                    // But this method wasn't added until rev 19 (Android 4.4)
+                    // Therefore, we only can query the entire device and see if
+                    // any InputDevice supports the keyCode.  This may result in
+                    // buttons being configured that don't exist on the specific
+                    // device, but I haven't found a better way yet.
+                    for (int keyCode: AndroidGamepadButtons) {
+                        logger.log(Level.INFO, "button[{0}]: {1}",
+                                new Object[]{keyCode, KeyCharacterMap.deviceHasKey(keyCode)});
+                        if (KeyCharacterMap.deviceHasKey(keyCode)) {
+                            // add button even though we aren't sure if the button
+                            // actually exists on this InputDevice
+                            logger.log(Level.INFO, "button[{0}] exists somewhere", keyCode);
+                            JoystickButton button = joystick.addButton(keyCode);
+                            logger.log(Level.INFO, "added button: {0}", button);
+                        }
+                    }
+
+                }
+            }
+        }
+
+
+        loaded = true;
+        return joysticks;
+    }
+
+    public boolean onGenericMotion(MotionEvent event) {
+        boolean consumed = false;
+//        logger.log(Level.INFO, "onGenericMotion event: {0}", event);
+        event.getDeviceId();
+        event.getSource();
+//        logger.log(Level.INFO, "deviceId: {0}, source: {1}", new Object[]{event.getDeviceId(), event.getSource()});
+        AndroidJoystick joystick = joystickIndex.get(event.getDeviceId());
+        if (joystick != null) {
+            for (int androidAxis: joystick.getAndroidAxes()) {
+                String axisName = MotionEvent.axisToString(androidAxis);
+                float value = event.getAxisValue(androidAxis);
+                int action = event.getAction();
+                if (action == MotionEvent.ACTION_MOVE) {
+//                    logger.log(Level.INFO, "MOVE axis num: {0}, axisName: {1}, value: {2}",
+//                            new Object[]{androidAxis, axisName, value});
+                    JoystickAxis axis = joystick.getAxis(androidAxis);
+                    if (axis != null) {
+//                        logger.log(Level.INFO, "MOVE axis num: {0}, axisName: {1}, value: {2}, deadzone: {3}",
+//                                new Object[]{androidAxis, axisName, value, axis.getDeadZone()});
+                        JoyAxisEvent axisEvent = new JoyAxisEvent(axis, value);
+                        joyInput.addEvent(axisEvent);
+                        consumed = true;
+                    } else {
+//                        logger.log(Level.INFO, "axis was null for axisName: {0}", axisName);
+                    }
+                } else {
+//                    logger.log(Level.INFO, "action: {0}", action);
+                }
+            }
+        }
+
+        return consumed;
+    }
+
+    public boolean onKey(KeyEvent event) {
+        boolean consumed = false;
+//        logger.log(Level.INFO, "onKey event: {0}", event);
+
+        event.getDeviceId();
+        event.getSource();
+        AndroidJoystick joystick = joystickIndex.get(event.getDeviceId());
+        if (joystick != null) {
+            JoystickButton button = joystick.getButton(event.getKeyCode());
+            boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
+            if (button != null) {
+                JoyButtonEvent buttonEvent = new JoyButtonEvent(button, pressed);
+                joyInput.addEvent(buttonEvent);
+                consumed = true;
+            } else {
+                JoystickButton newButton = joystick.addButton(event.getKeyCode());
+                JoyButtonEvent buttonEvent = new JoyButtonEvent(newButton, pressed);
+                joyInput.addEvent(buttonEvent);
+                consumed = true;
+            }
+        }
+
+        return consumed;
+    }
+
+    protected class AndroidJoystick extends AbstractJoystick {
+
+        private JoystickAxis nullAxis;
+        private InputDevice device;
+        private JoystickAxis xAxis;
+        private JoystickAxis yAxis;
+        private JoystickAxis povX;
+        private JoystickAxis povY;
+        private Map<Integer, JoystickAxis> axisIndex = new HashMap<Integer, JoystickAxis>();
+        private Map<Integer, JoystickButton> buttonIndex = new HashMap<Integer, JoystickButton>();
+
+        public AndroidJoystick( InputManager inputManager, JoyInput joyInput, InputDevice device,
+                               int joyId, String name ) {
+            super( inputManager, joyInput, joyId, name );
+
+            this.device = device;
+
+            this.nullAxis = new DefaultJoystickAxis( getInputManager(), this, -1,
+                                                     "Null", "null", false, false, 0 );
+            this.xAxis = nullAxis;
+            this.yAxis = nullAxis;
+            this.povX = nullAxis;
+            this.povY = nullAxis;
+        }
+
+        protected JoystickAxis getAxis(int androidAxis) {
+            return axisIndex.get(androidAxis);
+        }
+
+        protected Set<Integer> getAndroidAxes() {
+            return axisIndex.keySet();
+        }
+
+        protected JoystickButton getButton(int keyCode) {
+            return buttonIndex.get(keyCode);
+        }
+
+        protected JoystickButton addButton( int keyCode ) {
+
+//            logger.log(Level.FINE, "Adding button: {0}", keyCode);
+
+            String name = KeyEvent.keyCodeToString(keyCode);
+            String original = KeyEvent.keyCodeToString(keyCode);
+            // A/B/X/Y buttons
+            if (keyCode == KeyEvent.KEYCODE_BUTTON_Y) {
+                original = JoystickButton.BUTTON_0;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_B) {
+                original = JoystickButton.BUTTON_1;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_A) {
+                original = JoystickButton.BUTTON_2;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_X) {
+                original = JoystickButton.BUTTON_3;
+            // Front buttons  Some of these have the top ones and the bottoms ones flipped.
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_L1) {
+                original = JoystickButton.BUTTON_4;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_R1) {
+                original = JoystickButton.BUTTON_5;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_L2) {
+                original = JoystickButton.BUTTON_6;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_R2) {
+                original = JoystickButton.BUTTON_7;
+//            // Dpad buttons
+//            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+//                original = JoystickButton.BUTTON_8;
+//            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+//                original = JoystickButton.BUTTON_9;
+//            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+//                original = JoystickButton.BUTTON_8;
+//            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+//                original = JoystickButton.BUTTON_9;
+            // Select and start buttons
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
+                original = JoystickButton.BUTTON_8;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_START) {
+                original = JoystickButton.BUTTON_9;
+            // Joystick push buttons
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_THUMBL) {
+                original = JoystickButton.BUTTON_10;
+            } else if (keyCode == KeyEvent.KEYCODE_BUTTON_THUMBR) {
+                original = JoystickButton.BUTTON_11;
+            }
+
+            String logicalId = JoystickCompatibilityMappings.remapComponent( getName(), original );
+            if( logicalId == null ? original != null : !logicalId.equals(original) ) {
+                logger.log(Level.FINE, "Remapped: {0} to: {1}",
+                        new Object[]{original, logicalId});
+            }
+
+            JoystickButton button = new DefaultJoystickButton( getInputManager(), this, getButtonCount(),
+                                                               name, logicalId );
+            addButton(button);
+            buttonIndex.put( keyCode, button );
+            return button;
+        }
+
+        protected JoystickAxis addAxis(MotionRange motionRange) {
+
+            String name = MotionEvent.axisToString(motionRange.getAxis());
+
+            String original = MotionEvent.axisToString(motionRange.getAxis());
+            if (motionRange.getAxis() == MotionEvent.AXIS_X) {
+                original = JoystickAxis.X_AXIS;
+            } else if (motionRange.getAxis() == MotionEvent.AXIS_Y) {
+                original = JoystickAxis.Y_AXIS;
+            } else if (motionRange.getAxis() == MotionEvent.AXIS_Z) {
+                original = JoystickAxis.Z_AXIS;
+            } else if (motionRange.getAxis() == MotionEvent.AXIS_RZ) {
+                original = JoystickAxis.Z_ROTATION;
+            } else if (motionRange.getAxis() == MotionEvent.AXIS_HAT_X) {
+                original = JoystickAxis.POV_X;
+            } else if (motionRange.getAxis() == MotionEvent.AXIS_HAT_Y) {
+                original = JoystickAxis.POV_Y;
+            }
+            String logicalId = JoystickCompatibilityMappings.remapComponent( getName(), original );
+            if( logicalId == null ? original != null : !logicalId.equals(original) ) {
+                logger.log(Level.FINE, "Remapped: {0} to: {1}",
+                        new Object[]{original, logicalId});
+            }
+
+            JoystickAxis axis = new DefaultJoystickAxis(getInputManager(),
+                                                this,
+                                                getAxisCount(),
+                                                name,
+                                                logicalId,
+                                                true,
+                                                true,
+                                                motionRange.getFlat());
+
+            if (motionRange.getAxis() == MotionEvent.AXIS_X) {
+                xAxis = axis;
+            }
+            if (motionRange.getAxis() == MotionEvent.AXIS_Y) {
+                yAxis = axis;
+            }
+            if (motionRange.getAxis() == MotionEvent.AXIS_HAT_X) {
+                povX = axis;
+            }
+            if (motionRange.getAxis() == MotionEvent.AXIS_HAT_Y) {
+                povY = axis;
+            }
+
+            addAxis(axis);
+            axisIndex.put(motionRange.getAxis(), axis);
+            return axis;
+        }
+
+        @Override
+        public JoystickAxis getXAxis() {
+            return xAxis;
+        }
+
+        @Override
+        public JoystickAxis getYAxis() {
+            return yAxis;
+        }
+
+        @Override
+        public JoystickAxis getPovXAxis() {
+            return povX;
+        }
+
+        @Override
+        public JoystickAxis getPovYAxis() {
+            return povY;
+        }
+
+        @Override
+        public int getXAxisIndex(){
+            return xAxis.getAxisId();
+        }
+
+        @Override
+        public int getYAxisIndex(){
+            return yAxis.getAxisId();
+        }
+    }
+}

+ 0 - 140
jme3-android/src/main/java/com/jme3/input/android/AndroidKeyHandler.java

@@ -1,140 +0,0 @@
-/*
- * Copyright (c) 2009-2012 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.input.android;
-
-import android.content.Context;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import com.jme3.input.event.KeyInputEvent;
-import com.jme3.input.event.TouchEvent;
-import java.util.logging.Logger;
-
-/**
- * AndroidKeyHandler recieves onKey events from the Android system and creates
- * the jME KeyEvents.  onKey is used by Android to receive keys from the keyboard
- * or device buttons.  All key events are consumed by jME except for the Volume
- * buttons and menu button.
- *
- * This class also provides the functionality to display or hide the soft keyboard
- * for inputing single key events.  Use OGLESContext to display an dialog to type
- * in complete strings.
- *
- * @author iwgeric
- */
-public class AndroidKeyHandler implements View.OnKeyListener {
-    private static final Logger logger = Logger.getLogger(AndroidKeyHandler.class.getName());
-
-    private AndroidInputHandler androidInput;
-    private boolean sendKeyEvents = true;
-
-    public AndroidKeyHandler(AndroidInputHandler androidInput) {
-        this.androidInput = androidInput;
-    }
-
-    public void initialize() {
-    }
-
-    public void destroy() {
-    }
-
-    public void setView(View view) {
-        if (view != null) {
-            view.setOnKeyListener(this);
-        } else {
-            androidInput.getView().setOnKeyListener(null);
-        }
-    }
-
-    /**
-     * onKey gets called from android thread on key events
-     */
-    public boolean onKey(View view, int keyCode, KeyEvent event) {
-        if (androidInput.isInitialized() && view != androidInput.getView()) {
-            return false;
-        }
-
-        TouchEvent evt;
-        // TODO: get touch event from pool
-        if (event.getAction() == KeyEvent.ACTION_DOWN) {
-            evt = new TouchEvent();
-            evt.set(TouchEvent.Type.KEY_DOWN);
-            evt.setKeyCode(keyCode);
-            evt.setCharacters(event.getCharacters());
-            evt.setTime(event.getEventTime());
-
-            // Send the event
-            androidInput.addEvent(evt);
-
-        } else if (event.getAction() == KeyEvent.ACTION_UP) {
-            evt = new TouchEvent();
-            evt.set(TouchEvent.Type.KEY_UP);
-            evt.setKeyCode(keyCode);
-            evt.setCharacters(event.getCharacters());
-            evt.setTime(event.getEventTime());
-
-            // Send the event
-            androidInput.addEvent(evt);
-
-        }
-
-        if (androidInput.isSimulateKeyboard()) {
-            KeyInputEvent kie;
-            char unicodeChar = (char)event.getUnicodeChar();
-            int jmeKeyCode = AndroidKeyMapping.getJmeKey(keyCode);
-
-            boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
-            boolean repeating = pressed && event.getRepeatCount() > 0;
-
-            kie = new KeyInputEvent(jmeKeyCode, unicodeChar, pressed, repeating);
-            kie.setTime(event.getEventTime());
-            androidInput.addEvent(kie);
-//            logger.log(Level.FINE, "onKey keyCode: {0}, jmeKeyCode: {1}, pressed: {2}, repeating: {3}",
-//                    new Object[]{keyCode, jmeKeyCode, pressed, repeating});
-//            logger.log(Level.FINE, "creating KeyInputEvent: {0}", kie);
-        }
-
-        // consume all keys ourself except Volume Up/Down and Menu
-        //   Don't do Menu so that typical Android Menus can be created and used
-        //   by the user in MainActivity
-        if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) ||
-                (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) ||
-                (keyCode == KeyEvent.KEYCODE_MENU)) {
-            return false;
-        } else {
-            return true;
-        }
-
-   }
-
-}

+ 11 - 6
jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java

@@ -37,13 +37,14 @@ import java.util.logging.Logger;
 
 /**
  * AndroidKeyMapping is just a utility to convert the Android keyCodes into
- * jME KeyCodes received in jME's KeyEvent will match between Desktop and Android.
- * 
+ * jME KeyCodes so that events received in jME's KeyEvent will match between
+ * Desktop and Android.
+ *
  * @author iwgeric
  */
 public class AndroidKeyMapping {
     private static final Logger logger = Logger.getLogger(AndroidKeyMapping.class.getName());
-    
+
     private static final int[] ANDROID_TO_JME = {
         0x0, // unknown
         0x0, // key code soft left
@@ -141,9 +142,13 @@ public class AndroidKeyMapping {
         0x0,//media fastforward
         0x0,//mute
     };
-    
+
     public static int getJmeKey(int androidKey) {
-        return ANDROID_TO_JME[androidKey];
+        if (androidKey > ANDROID_TO_JME.length) {
+            return androidKey;
+        } else {
+            return ANDROID_TO_JME[androidKey];
+        }
     }
-    
+
 }

+ 24 - 24
jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java

@@ -75,15 +75,15 @@ import java.util.logging.Logger;
 public class AndroidSensorJoyInput implements SensorEventListener {
     private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName());
 
-    private AndroidJoyInputHandler joyHandler;
+    private AndroidJoyInput joyInput;
     private SensorManager sensorManager = null;
     private WindowManager windowManager = null;
     private IntMap<SensorData> sensors = new IntMap<SensorData>();
     private int lastRotation = 0;
     private boolean loaded = false;
 
-    public AndroidSensorJoyInput(AndroidJoyInputHandler joyHandler) {
-        this.joyHandler = joyHandler;
+    public AndroidSensorJoyInput(AndroidJoyInput joyInput) {
+        this.joyInput = joyInput;
     }
 
     /**
@@ -96,7 +96,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         int sensorAccuracy = -1;
         float[] lastValues;
         final Object valuesLock = new Object();
-        ArrayList<AndroidJoystickAxis> axes = new ArrayList<AndroidJoystickAxis>();
+        ArrayList<AndroidSensorJoystickAxis> axes = new ArrayList<AndroidSensorJoystickAxis>();
         boolean enabled = false;
         boolean haveData = false;
 
@@ -306,7 +306,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
      */
     private boolean updateOrientation() {
         SensorData sensorData;
-        AndroidJoystickAxis axis;
+        AndroidSensorJoystickAxis axis;
         final float[] curInclinationMat = new float[16];
         final float[] curRotationMat = new float[16];
         final float[] rotatedRotationMat = new float[16];
@@ -374,7 +374,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                                 sensorData.haveData = true;
                             } else {
                                 if (axis.isChanged()) {
-                                    joyHandler.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
+                                    joyInput.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
                                 }
                             }
                         }
@@ -401,10 +401,10 @@ public class AndroidSensorJoyInput implements SensorEventListener {
 
     public Joystick loadJoystick(int joyId, InputManager inputManager) {
         SensorData sensorData;
-        AndroidJoystickAxis axis;
+        AndroidSensorJoystickAxis axis;
 
-        AndroidJoystick joystick = new AndroidJoystick(inputManager,
-                                    joyHandler,
+        AndroidSensorJoystick joystick = new AndroidSensorJoystick(inputManager,
+                                    joyInput,
                                     joyId,
                                     "AndroidSensorsJoystick");
 
@@ -522,15 +522,15 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         if (!loaded) {
             return;
         }
-        logger.log(Level.FINE, "onSensorChanged for {0}: accuracy: {1}, values: {2}",
-                new Object[]{se.sensor.getName(), se.accuracy, se.values});
+//        logger.log(Level.FINE, "onSensorChanged for {0}: accuracy: {1}, values: {2}",
+//                new Object[]{se.sensor.getName(), se.accuracy, se.values});
 
         int sensorType = se.sensor.getType();
 
         SensorData sensorData = sensors.get(sensorType);
         if (sensorData != null) {
-            logger.log(Level.FINE, "sensorData name: {0}, enabled: {1}, unreliable: {2}",
-                    new Object[]{sensorData.sensor.getName(), sensorData.enabled, sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE});
+//            logger.log(Level.FINE, "sensorData name: {0}, enabled: {1}, unreliable: {2}",
+//                    new Object[]{sensorData.sensor.getName(), sensorData.enabled, sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE});
         }
         if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) {
 
@@ -543,8 +543,8 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                 }
             }
 
-            if (sensorData != null && sensorData.axes.size() > 0) {
-                AndroidJoystickAxis axis;
+            if (sensorData.axes.size() > 0) {
+                AndroidSensorJoystickAxis axis;
                 for (int i=0; i<se.values.length; i++) {
                     axis = sensorData.axes.get(i);
                     if (axis != null) {
@@ -554,8 +554,8 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                         } else {
                             if (axis.isChanged()) {
                                 JoyAxisEvent event = new JoyAxisEvent(axis, axis.getJoystickAxisValue());
-                                logger.log(Level.INFO, "adding JoyAxisEvent: {0}", event);
-                                joyHandler.addEvent(event);
+//                                logger.log(Level.INFO, "adding JoyAxisEvent: {0}", event);
+                                joyInput.addEvent(event);
 //                                joyHandler.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
                             }
                         }
@@ -585,14 +585,14 @@ public class AndroidSensorJoyInput implements SensorEventListener {
 
     // End of SensorEventListener methods
 
-    protected class AndroidJoystick extends AbstractJoystick {
+    protected class AndroidSensorJoystick extends AbstractJoystick {
         private JoystickAxis nullAxis;
         private JoystickAxis xAxis;
         private JoystickAxis yAxis;
         private JoystickAxis povX;
         private JoystickAxis povY;
 
-        public AndroidJoystick( InputManager inputManager, JoyInput joyInput,
+        public AndroidSensorJoystick( InputManager inputManager, JoyInput joyInput,
                                 int joyId, String name){
 
             super( inputManager, joyInput, joyId, name );
@@ -606,10 +606,10 @@ public class AndroidSensorJoyInput implements SensorEventListener {
 
         }
 
-        protected AndroidJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
-            AndroidJoystickAxis axis;
+        protected AndroidSensorJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
+            AndroidSensorJoystickAxis axis;
 
-            axis = new AndroidJoystickAxis(
+            axis = new AndroidSensorJoystickAxis(
                     getInputManager(),          // InputManager (InputManager)
                     this,                       // parent Joystick (Joystick)
                     axisNum,                    // Axis Index (int)
@@ -654,7 +654,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
 
     }
 
-    public class AndroidJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
+    public class AndroidSensorJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
         float zeroRawValue = 0f;
         float curRawValue = 0f;
         float lastRawValue = 0f;
@@ -662,7 +662,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         float maxRawValue = FastMath.HALF_PI;
         boolean enabled = true;
 
-        public AndroidJoystickAxis(InputManager inputManager, Joystick parent,
+        public AndroidSensorJoystickAxis(InputManager inputManager, Joystick parent,
                            int axisIndex, String name, String logicalId,
                            boolean isAnalog, boolean isRelative, float deadZone,
                            float maxRawValue) {

+ 0 - 257
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler.java

@@ -1,257 +0,0 @@
-/*
- * Copyright (c) 2009-2012 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.input.android;
-
-import android.view.MotionEvent;
-import android.view.View;
-import com.jme3.input.event.InputEvent;
-import com.jme3.input.event.MouseButtonEvent;
-import com.jme3.input.event.MouseMotionEvent;
-import com.jme3.input.event.TouchEvent;
-import static com.jme3.input.event.TouchEvent.Type.DOWN;
-import static com.jme3.input.event.TouchEvent.Type.MOVE;
-import static com.jme3.input.event.TouchEvent.Type.UP;
-import com.jme3.math.Vector2f;
-import java.util.HashMap;
-import java.util.logging.Logger;
-
-/**
- * AndroidTouchHandler is the base class that receives touch inputs from the 
- * Android system and creates the TouchEvents for jME.  This class is designed
- * to handle the base touch events for Android rev 9 (Android 2.3).  This is
- * extended by other classes to add features that were introducted after
- * Android rev 9.
- * 
- * @author iwgeric
- */
-public class AndroidTouchHandler implements View.OnTouchListener {
-    private static final Logger logger = Logger.getLogger(AndroidTouchHandler.class.getName());
-    
-    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
-
-    protected int numPointers = 0;
-    
-    protected AndroidInputHandler androidInput;
-    protected AndroidGestureHandler gestureHandler;
-
-    public AndroidTouchHandler(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
-        this.androidInput = androidInput;
-        this.gestureHandler = gestureHandler;
-    }
-
-    public void initialize() {
-    }
-    
-    public void destroy() {
-        setView(null);
-    }
-    
-    public void setView(View view) {
-        if (view != null) {
-            view.setOnTouchListener(this);
-        } else {
-            androidInput.getView().setOnTouchListener(null);
-        }
-    }
-    
-    protected int getPointerIndex(MotionEvent event) {
-        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
-                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-    }
-    
-    protected int getPointerId(MotionEvent event) {
-        return event.getPointerId(getPointerIndex(event));
-    }
-    
-    protected int getAction(MotionEvent event) {
-        return event.getAction() & MotionEvent.ACTION_MASK;
-    }
-    
-    /**
-     * onTouch gets called from android thread on touch events
-     */
-    public boolean onTouch(View view, MotionEvent event) {
-        if (!androidInput.isInitialized() || view != androidInput.getView()) {
-            return false;
-        }
-        
-        boolean bWasHandled = false;
-        TouchEvent touch = null;
-        //    System.out.println("native : " + event.getAction());
-        int action = getAction(event);
-        int pointerIndex = getPointerIndex(event);
-        int pointerId = getPointerId(event);
-        Vector2f lastPos = lastPositions.get(pointerId);
-        float jmeX;
-        float jmeY;
-        
-        numPointers = event.getPointerCount();
-
-        // final int historySize = event.getHistorySize();
-        //final int pointerCount = event.getPointerCount();
-        switch (getAction(event)) {
-            case MotionEvent.ACTION_POINTER_DOWN:
-            case MotionEvent.ACTION_DOWN:
-                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
-                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
-                touch = androidInput.getFreeTouchEvent();
-                touch.set(TouchEvent.Type.DOWN, jmeX, jmeY, 0, 0);
-                touch.setPointerId(pointerId);
-                touch.setTime(event.getEventTime());
-                touch.setPressure(event.getPressure(pointerIndex));
-
-                lastPos = new Vector2f(jmeX, jmeY);
-                lastPositions.put(pointerId, lastPos);
-
-                processEvent(touch);
-
-                bWasHandled = true;
-                break;
-            case MotionEvent.ACTION_POINTER_UP:
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
-                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
-                touch = androidInput.getFreeTouchEvent();
-                touch.set(TouchEvent.Type.UP, jmeX, jmeY, 0, 0);
-                touch.setPointerId(pointerId);
-                touch.setTime(event.getEventTime());
-                touch.setPressure(event.getPressure(pointerIndex));
-                lastPositions.remove(pointerId);
-
-                processEvent(touch);
-
-                bWasHandled = true;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                // Convert all pointers into events
-                for (int p = 0; p < event.getPointerCount(); p++) {
-                    jmeX = androidInput.getJmeX(event.getX(p));
-                    jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
-                    lastPos = lastPositions.get(event.getPointerId(p));
-                    if (lastPos == null) {
-                        lastPos = new Vector2f(jmeX, jmeY);
-                        lastPositions.put(event.getPointerId(p), lastPos);
-                    }
-
-                    float dX = jmeX - lastPos.x;
-                    float dY = jmeY - lastPos.y;
-                    if (dX != 0 || dY != 0) {
-                        touch = androidInput.getFreeTouchEvent();
-                        touch.set(TouchEvent.Type.MOVE, jmeX, jmeY, dX, dY);
-                        touch.setPointerId(event.getPointerId(p));
-                        touch.setTime(event.getEventTime());
-                        touch.setPressure(event.getPressure(p));
-                        lastPos.set(jmeX, jmeY);
-
-                        processEvent(touch);
-
-                        bWasHandled = true;
-                    }
-                }
-                break;
-            case MotionEvent.ACTION_OUTSIDE:
-                break;
-
-        }
-
-        // Try to detect gestures
-        if (gestureHandler != null) {
-            gestureHandler.detectGesture(event);
-        }
-
-        return bWasHandled;
-    }
-
-    protected void processEvent(TouchEvent event) {
-        // Add the touch event
-        androidInput.addEvent(event);
-        // MouseEvents do not support multi-touch, so only evaluate 1 finger pointer events
-        if (androidInput.isSimulateMouse() && numPointers == 1) {
-            InputEvent mouseEvent = generateMouseEvent(event);
-            if (mouseEvent != null) {
-                // Add the mouse event
-                androidInput.addEvent(mouseEvent);
-            }
-        }
-        
-    }
-
-    // TODO: Ring Buffer for mouse events?
-    protected InputEvent generateMouseEvent(TouchEvent event) {
-        InputEvent inputEvent = null;
-        int newX;
-        int newY;
-        int newDX;
-        int newDY;
-
-        if (androidInput.isMouseEventsInvertX()) {
-            newX = (int) (androidInput.invertX(event.getX()));
-            newDX = (int)event.getDeltaX() * -1;
-        } else {
-            newX = (int) event.getX();
-            newDX = (int)event.getDeltaX();
-        }
-
-        if (androidInput.isMouseEventsInvertY()) {
-            newY = (int) (androidInput.invertY(event.getY()));
-            newDY = (int)event.getDeltaY() * -1;
-        } else {
-            newY = (int) event.getY();
-            newDY = (int)event.getDeltaY();
-        }
-
-        switch (event.getType()) {
-            case DOWN:
-                // Handle mouse down event
-                inputEvent = new MouseButtonEvent(0, true, newX, newY);
-                inputEvent.setTime(event.getTime());
-                break;
-
-            case UP:
-                // Handle mouse up event
-                inputEvent = new MouseButtonEvent(0, false, newX, newY);
-                inputEvent.setTime(event.getTime());
-                break;
-
-            case HOVER_MOVE:
-            case MOVE:
-                inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
-                inputEvent.setTime(event.getTime());
-                break;
-        }
-
-        return inputEvent;
-    }
-    
-}

+ 475 - 0
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java

@@ -0,0 +1,475 @@
+/*
+ * Copyright (c) 2009-2012 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.input.android;
+
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.TouchInput;
+import com.jme3.input.event.InputEvent;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import static com.jme3.input.event.TouchEvent.Type.DOWN;
+import static com.jme3.input.event.TouchEvent.Type.MOVE;
+import static com.jme3.input.event.TouchEvent.Type.UP;
+import com.jme3.math.Vector2f;
+import com.jme3.system.AppSettings;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * AndroidTouchInput is the base class that receives touch inputs from the
+ * Android system and creates the TouchEvents for jME.  This class is designed
+ * to handle the base touch events for Android rev 9 (Android 2.3).  This is
+ * extended by other classes to add features that were introducted after
+ * Android rev 9.
+ *
+ * @author iwgeric
+ */
+public class AndroidTouchInput implements TouchInput {
+    private static final Logger logger = Logger.getLogger(AndroidTouchInput.class.getName());
+
+    private boolean mouseEventsEnabled = true;
+    private boolean mouseEventsInvertX = false;
+    private boolean mouseEventsInvertY = false;
+    private boolean keyboardEventsEnabled = false;
+    private boolean dontSendHistory = false;
+
+    protected int numPointers = 0;
+    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
+    final private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<InputEvent>();
+    private final static int MAX_TOUCH_EVENTS = 1024;
+    private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS);
+    private float scaleX = 1f;
+    private float scaleY = 1f;
+
+    private boolean initialized = false;
+    private RawInputListener listener = null;
+
+    private GestureDetector gestureDetector;
+    private ScaleGestureDetector scaleDetector;
+
+    protected AndroidInputHandler androidInput;
+
+    public AndroidTouchInput(AndroidInputHandler androidInput) {
+        this.androidInput = androidInput;
+    }
+
+    public GestureDetector getGestureDetector() {
+        return gestureDetector;
+    }
+
+    public void setGestureDetector(GestureDetector gestureDetector) {
+        this.gestureDetector = gestureDetector;
+    }
+
+    public ScaleGestureDetector getScaleDetector() {
+        return scaleDetector;
+    }
+
+    public void setScaleDetector(ScaleGestureDetector scaleDetector) {
+        this.scaleDetector = scaleDetector;
+    }
+
+    public float invertX(float origX) {
+        return getJmeX(androidInput.getView().getWidth()) - origX;
+    }
+
+    public float invertY(float origY) {
+        return getJmeY(androidInput.getView().getHeight()) - origY;
+    }
+
+    public float getJmeX(float origX) {
+        return origX * scaleX;
+    }
+
+    public float getJmeY(float origY) {
+        return origY * scaleY;
+    }
+
+    public void loadSettings(AppSettings settings) {
+        keyboardEventsEnabled = settings.isEmulateKeyboard();
+        mouseEventsEnabled = settings.isEmulateMouse();
+        mouseEventsInvertX = settings.isEmulateMouseFlipX();
+        mouseEventsInvertY = settings.isEmulateMouseFlipY();
+
+        // view width and height are 0 until the view is displayed on the screen
+        if (androidInput.getView().getWidth() != 0 && androidInput.getView().getHeight() != 0) {
+            scaleX = (float)settings.getWidth() / (float)androidInput.getView().getWidth();
+            scaleY = (float)settings.getHeight() / (float)androidInput.getView().getHeight();
+        }
+        logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
+                new Object[]{scaleX, scaleY});
+
+
+    }
+
+
+    protected int getPointerIndex(MotionEvent event) {
+        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+    }
+
+    protected int getPointerId(MotionEvent event) {
+        return event.getPointerId(getPointerIndex(event));
+    }
+
+    protected int getAction(MotionEvent event) {
+        return event.getAction() & MotionEvent.ACTION_MASK;
+    }
+
+    public boolean onTouch(MotionEvent event) {
+        if (!isInitialized()) {
+            return false;
+        }
+
+        boolean bWasHandled = false;
+        TouchEvent touch = null;
+        //    System.out.println("native : " + event.getAction());
+        int action = getAction(event);
+        int pointerIndex = getPointerIndex(event);
+        int pointerId = getPointerId(event);
+        Vector2f lastPos = lastPositions.get(pointerId);
+        float jmeX;
+        float jmeY;
+
+        numPointers = event.getPointerCount();
+
+        // final int historySize = event.getHistorySize();
+        //final int pointerCount = event.getPointerCount();
+        switch (getAction(event)) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_DOWN:
+                jmeX = getJmeX(event.getX(pointerIndex));
+                jmeY = invertY(getJmeY(event.getY(pointerIndex)));
+                touch = getFreeTouchEvent();
+                touch.set(TouchEvent.Type.DOWN, jmeX, jmeY, 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+
+                lastPos = new Vector2f(jmeX, jmeY);
+                lastPositions.put(pointerId, lastPos);
+
+                addEvent(touch);
+                addEvent(generateMouseEvent(touch));
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                jmeX = getJmeX(event.getX(pointerIndex));
+                jmeY = invertY(getJmeY(event.getY(pointerIndex)));
+                touch = getFreeTouchEvent();
+                touch.set(TouchEvent.Type.UP, jmeX, jmeY, 0, 0);
+                touch.setPointerId(pointerId);
+                touch.setTime(event.getEventTime());
+                touch.setPressure(event.getPressure(pointerIndex));
+                lastPositions.remove(pointerId);
+
+                addEvent(touch);
+                addEvent(generateMouseEvent(touch));
+
+                bWasHandled = true;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Convert all pointers into events
+                for (int p = 0; p < event.getPointerCount(); p++) {
+                    jmeX = getJmeX(event.getX(p));
+                    jmeY = invertY(getJmeY(event.getY(p)));
+                    lastPos = lastPositions.get(event.getPointerId(p));
+                    if (lastPos == null) {
+                        lastPos = new Vector2f(jmeX, jmeY);
+                        lastPositions.put(event.getPointerId(p), lastPos);
+                    }
+
+                    float dX = jmeX - lastPos.x;
+                    float dY = jmeY - lastPos.y;
+                    if (dX != 0 || dY != 0) {
+                        touch = getFreeTouchEvent();
+                        touch.set(TouchEvent.Type.MOVE, jmeX, jmeY, dX, dY);
+                        touch.setPointerId(event.getPointerId(p));
+                        touch.setTime(event.getEventTime());
+                        touch.setPressure(event.getPressure(p));
+                        lastPos.set(jmeX, jmeY);
+
+                        addEvent(touch);
+                        addEvent(generateMouseEvent(touch));
+
+                        bWasHandled = true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_OUTSIDE:
+                break;
+
+        }
+
+        // Try to detect gestures
+        if (gestureDetector != null) {
+            gestureDetector.onTouchEvent(event);
+        }
+        if (scaleDetector != null) {
+            scaleDetector.onTouchEvent(event);
+        }
+
+        return bWasHandled;
+    }
+
+    // TODO: Ring Buffer for mouse events?
+    public InputEvent generateMouseEvent(TouchEvent event) {
+        InputEvent inputEvent = null;
+        int newX;
+        int newY;
+        int newDX;
+        int newDY;
+
+        // MouseEvents do not support multi-touch, so only evaluate 1 finger pointer events
+        if (!isSimulateMouse() || numPointers > 1) {
+            return null;
+        }
+
+
+        if (isMouseEventsInvertX()) {
+            newX = (int) (invertX(event.getX()));
+            newDX = (int)event.getDeltaX() * -1;
+        } else {
+            newX = (int) event.getX();
+            newDX = (int)event.getDeltaX();
+        }
+
+        if (isMouseEventsInvertY()) {
+            newY = (int) (invertY(event.getY()));
+            newDY = (int)event.getDeltaY() * -1;
+        } else {
+            newY = (int) event.getY();
+            newDY = (int)event.getDeltaY();
+        }
+
+        switch (event.getType()) {
+            case DOWN:
+                // Handle mouse down event
+                inputEvent = new MouseButtonEvent(0, true, newX, newY);
+                inputEvent.setTime(event.getTime());
+                break;
+
+            case UP:
+                // Handle mouse up event
+                inputEvent = new MouseButtonEvent(0, false, newX, newY);
+                inputEvent.setTime(event.getTime());
+                break;
+
+            case HOVER_MOVE:
+            case MOVE:
+                inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, (int)event.getScaleSpan(), (int)event.getDeltaScaleSpan());
+                inputEvent.setTime(event.getTime());
+                break;
+        }
+
+        return inputEvent;
+    }
+
+
+    public boolean onKey(KeyEvent event) {
+        if (!isInitialized()) {
+            return false;
+        }
+
+        TouchEvent evt;
+        // TODO: get touch event from pool
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            evt = new TouchEvent();
+            evt.set(TouchEvent.Type.KEY_DOWN);
+            evt.setKeyCode(event.getKeyCode());
+            evt.setCharacters(event.getCharacters());
+            evt.setTime(event.getEventTime());
+
+            // Send the event
+            addEvent(evt);
+
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            evt = new TouchEvent();
+            evt.set(TouchEvent.Type.KEY_UP);
+            evt.setKeyCode(event.getKeyCode());
+            evt.setCharacters(event.getCharacters());
+            evt.setTime(event.getEventTime());
+
+            // Send the event
+            addEvent(evt);
+
+        }
+
+        if (isSimulateKeyboard()) {
+            KeyInputEvent kie;
+            char unicodeChar = (char)event.getUnicodeChar();
+            int jmeKeyCode = AndroidKeyMapping.getJmeKey(event.getKeyCode());
+
+            boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
+            boolean repeating = pressed && event.getRepeatCount() > 0;
+
+            kie = new KeyInputEvent(jmeKeyCode, unicodeChar, pressed, repeating);
+            kie.setTime(event.getEventTime());
+            addEvent(kie);
+//            logger.log(Level.FINE, "onKey keyCode: {0}, jmeKeyCode: {1}, pressed: {2}, repeating: {3}",
+//                    new Object[]{event.getKeyCode(), jmeKeyCode, pressed, repeating});
+//            logger.log(Level.FINE, "creating KeyInputEvent: {0}", kie);
+        }
+
+        // consume all keys ourself except Volume Up/Down and Menu
+        //   Don't do Menu so that typical Android Menus can be created and used
+        //   by the user in MainActivity
+        if ((event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ||
+                (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN) ||
+                (event.getKeyCode() == KeyEvent.KEYCODE_MENU)) {
+            return false;
+        } else {
+            return true;
+        }
+
+   }
+
+
+
+
+        // -----------------------------------------
+    // JME3 Input interface
+    @Override
+    public void initialize() {
+        touchEventPool.initialize();
+
+        initialized = true;
+    }
+
+    @Override
+    public void destroy() {
+        initialized = false;
+
+        touchEventPool.destroy();
+
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    @Override
+    public void setInputListener(RawInputListener listener) {
+        this.listener = listener;
+    }
+
+    @Override
+    public long getInputTimeNanos() {
+        return System.nanoTime();
+    }
+
+    @Override
+    public void update() {
+        if (listener != null) {
+            InputEvent inputEvent;
+
+            while ((inputEvent = inputEventQueue.poll()) != null) {
+                if (inputEvent instanceof TouchEvent) {
+                    listener.onTouchEvent((TouchEvent)inputEvent);
+                } else if (inputEvent instanceof MouseButtonEvent) {
+                    listener.onMouseButtonEvent((MouseButtonEvent)inputEvent);
+                } else if (inputEvent instanceof MouseMotionEvent) {
+                    listener.onMouseMotionEvent((MouseMotionEvent)inputEvent);
+                } else if (inputEvent instanceof KeyInputEvent) {
+                    listener.onKeyEvent((KeyInputEvent)inputEvent);
+                }
+            }
+        }
+    }
+
+    // -----------------------------------------
+
+    public TouchEvent getFreeTouchEvent() {
+            return touchEventPool.getNextFreeEvent();
+    }
+
+    public void addEvent(InputEvent event) {
+        if (event == null) {
+            return;
+        }
+
+        logger.log(Level.INFO, "event: {0}", event);
+
+        inputEventQueue.add(event);
+        if (event instanceof TouchEvent) {
+            touchEventPool.storeEvent((TouchEvent)event);
+        }
+
+    }
+
+    @Override
+    public void setSimulateMouse(boolean simulate) {
+        this.mouseEventsEnabled = simulate;
+    }
+
+    @Override
+    public boolean isSimulateMouse() {
+        return mouseEventsEnabled;
+    }
+
+    public boolean isMouseEventsInvertX() {
+        return mouseEventsInvertX;
+    }
+
+    public boolean isMouseEventsInvertY() {
+        return mouseEventsInvertY;
+    }
+
+    @Override
+    public void setSimulateKeyboard(boolean simulate) {
+        this.keyboardEventsEnabled = simulate;
+    }
+
+    @Override
+    public boolean isSimulateKeyboard() {
+        return keyboardEventsEnabled;
+    }
+
+    @Override
+    public void setOmitHistoricEvents(boolean dontSendHistory) {
+        this.dontSendHistory = dontSendHistory;
+    }
+
+}

+ 30 - 46
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchHandler14.java → jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java

@@ -33,7 +33,6 @@
 package com.jme3.input.android;
 
 import android.view.MotionEvent;
-import android.view.View;
 import com.jme3.input.event.TouchEvent;
 import com.jme3.math.Vector2f;
 import java.util.HashMap;
@@ -41,36 +40,20 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
- * AndroidTouchHandler14 is an extension of AndroidTouchHander that adds the
- * Android touch event functionality between Android rev 9 (Android 2.3) and 
- * Android rev 14 (Android 4.0).
- * 
+ * AndroidTouchHandler14 extends AndroidTouchHandler to process the onHover
+ * events added in Android rev 14 (Android 4.0).
+ *
  * @author iwgeric
  */
-public class AndroidTouchHandler14 extends AndroidTouchHandler implements 
-        View.OnHoverListener {
-    private static final Logger logger = Logger.getLogger(AndroidTouchHandler14.class.getName());
+public class AndroidTouchInput14 extends AndroidTouchInput {
+    private static final Logger logger = Logger.getLogger(AndroidTouchInput14.class.getName());
     final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<Integer, Vector2f>();
-    
-    public AndroidTouchHandler14(AndroidInputHandler androidInput, AndroidGestureHandler gestureHandler) {
-        super(androidInput, gestureHandler);
-    }
 
-    @Override
-    public void setView(View view) {
-        if (view != null) {
-            view.setOnHoverListener(this);
-        } else {
-            androidInput.getView().setOnHoverListener(null);
-        }
-        super.setView(view);
+    public AndroidTouchInput14(AndroidInputHandler androidInput) {
+        super(androidInput);
     }
-    
-    public boolean onHover(View view, MotionEvent event) {
-        if (view == null || view != androidInput.getView()) {
-            return false;
-        }
-        
+
+    public boolean onHover(MotionEvent event) {
         boolean consumed = false;
         int action = getAction(event);
         int pointerId = getPointerId(event);
@@ -78,34 +61,34 @@ public class AndroidTouchHandler14 extends AndroidTouchHandler implements
         Vector2f lastPos = lastHoverPositions.get(pointerId);
         float jmeX;
         float jmeY;
-        
+
         numPointers = event.getPointerCount();
-        
-        logger.log(Level.INFO, "onHover pointerId: {0}, action: {1}, x: {2}, y: {3}, numPointers: {4}", 
-                new Object[]{pointerId, action, event.getX(), event.getY(), event.getPointerCount()});
+
+//        logger.log(Level.INFO, "onHover pointerId: {0}, action: {1}, x: {2}, y: {3}, numPointers: {4}",
+//                new Object[]{pointerId, action, event.getX(), event.getY(), event.getPointerCount()});
 
         TouchEvent touchEvent;
         switch (action) {
             case MotionEvent.ACTION_HOVER_ENTER:
-                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
-                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
-                touchEvent = androidInput.getFreeTouchEvent();
+                jmeX = getJmeX(event.getX(pointerIndex));
+                jmeY = invertY(getJmeY(event.getY(pointerIndex)));
+                touchEvent = getFreeTouchEvent();
                 touchEvent.set(TouchEvent.Type.HOVER_START, jmeX, jmeY, 0, 0);
                 touchEvent.setPointerId(pointerId);
                 touchEvent.setTime(event.getEventTime());
                 touchEvent.setPressure(event.getPressure(pointerIndex));
-                
+
                 lastPos = new Vector2f(jmeX, jmeY);
                 lastHoverPositions.put(pointerId, lastPos);
-                
-                processEvent(touchEvent);
+
+                addEvent(touchEvent);
                 consumed = true;
                 break;
             case MotionEvent.ACTION_HOVER_MOVE:
                 // Convert all pointers into events
                 for (int p = 0; p < event.getPointerCount(); p++) {
-                    jmeX = androidInput.getJmeX(event.getX(p));
-                    jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(p)));
+                    jmeX = getJmeX(event.getX(p));
+                    jmeY = invertY(getJmeY(event.getY(p)));
                     lastPos = lastHoverPositions.get(event.getPointerId(p));
                     if (lastPos == null) {
                         lastPos = new Vector2f(jmeX, jmeY);
@@ -115,38 +98,39 @@ public class AndroidTouchHandler14 extends AndroidTouchHandler implements
                     float dX = jmeX - lastPos.x;
                     float dY = jmeY - lastPos.y;
                     if (dX != 0 || dY != 0) {
-                        touchEvent = androidInput.getFreeTouchEvent();
+                        touchEvent = getFreeTouchEvent();
                         touchEvent.set(TouchEvent.Type.HOVER_MOVE, jmeX, jmeY, dX, dY);
                         touchEvent.setPointerId(event.getPointerId(p));
                         touchEvent.setTime(event.getEventTime());
                         touchEvent.setPressure(event.getPressure(p));
                         lastPos.set(jmeX, jmeY);
 
-                        processEvent(touchEvent);
+                        addEvent(touchEvent);
 
                     }
                 }
                 consumed = true;
                 break;
             case MotionEvent.ACTION_HOVER_EXIT:
-                jmeX = androidInput.getJmeX(event.getX(pointerIndex));
-                jmeY = androidInput.invertY(androidInput.getJmeY(event.getY(pointerIndex)));
-                touchEvent = androidInput.getFreeTouchEvent();
+                jmeX = getJmeX(event.getX(pointerIndex));
+                jmeY = invertY(getJmeY(event.getY(pointerIndex)));
+                touchEvent = getFreeTouchEvent();
                 touchEvent.set(TouchEvent.Type.HOVER_END, jmeX, jmeY, 0, 0);
                 touchEvent.setPointerId(pointerId);
                 touchEvent.setTime(event.getEventTime());
                 touchEvent.setPressure(event.getPressure(pointerIndex));
                 lastHoverPositions.remove(pointerId);
 
-                processEvent(touchEvent);
+                addEvent(touchEvent);
                 consumed = true;
                 break;
             default:
                 consumed = false;
                 break;
         }
-        
+
         return consumed;
+
     }
-    
+
 }

+ 3 - 0
jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java

@@ -43,6 +43,9 @@ import java.nio.ShortBuffer;
 
 public class AndroidGL implements GL, GLExt {
 
+    public void resetStats() {
+    }
+    
     private static int getLimitBytes(ByteBuffer buffer) {
         checkLimit(buffer);
         return buffer.limit();

+ 11 - 15
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java

@@ -47,13 +47,14 @@ import android.widget.EditText;
 import android.widget.FrameLayout;
 import com.jme3.input.*;
 import com.jme3.input.android.AndroidInputHandler;
-import com.jme3.input.android.AndroidJoyInputHandler;
+import com.jme3.input.android.AndroidInputHandler14;
 import com.jme3.input.controls.SoftTextDialogInputListener;
 import com.jme3.input.dummy.DummyKeyInput;
 import com.jme3.input.dummy.DummyMouseInput;
 import com.jme3.renderer.android.AndroidGL;
 import com.jme3.renderer.opengl.GL;
 import com.jme3.renderer.opengl.GLExt;
+import com.jme3.renderer.opengl.GLFbo;
 import com.jme3.renderer.opengl.GLRenderer;
 import com.jme3.system.*;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -75,7 +76,6 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
     protected SystemListener listener;
     protected boolean autoFlush = true;
     protected AndroidInputHandler androidInput;
-    protected AndroidJoyInputHandler androidJoyInput = null;
     protected long minFrameDuration = 0;                   // No FPS cap
     protected long lastUpdateTime = 0;
 
@@ -111,18 +111,17 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
 
         // Start to set up the view
         GLSurfaceView view = new GLSurfaceView(context);
+        logger.log(Level.INFO, "Android Build Version: {0}", Build.VERSION.SDK_INT);
         if (androidInput == null) {
-            androidInput = new AndroidInputHandler();
+            if (Build.VERSION.SDK_INT >= 14) {
+                androidInput = new AndroidInputHandler14();
+            } else if (Build.VERSION.SDK_INT >= 9){
+                androidInput = new AndroidInputHandler();
+            }
         }
         androidInput.setView(view);
         androidInput.loadSettings(settings);
 
-        if (androidJoyInput == null) {
-            androidJoyInput = new AndroidJoyInputHandler();
-        }
-        androidJoyInput.setView(view);
-        androidJoyInput.loadSettings(settings);
-
         // setEGLContextClientVersion must be set before calling setRenderer
         // this means it cannot be set in AndroidConfigChooser (too late)
         view.setEGLContextClientVersion(2);
@@ -198,7 +197,7 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
         Object gl = new AndroidGL();
         // gl = GLTracer.createGlesTracer((GL)gl, (GLExt)gl);
         // gl = new GLDebugES((GL)gl, (GLExt)gl);
-        renderer = new GLRenderer((GL)gl, (GLExt)gl);
+        renderer = new GLRenderer((GL)gl, (GLExt)gl, (GLFbo)gl);
         renderer.initialize();
 
         JmeSystem.setSoftTextDialogInput(this);
@@ -235,9 +234,6 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
         if (androidInput != null) {
             androidInput.loadSettings(settings);
         }
-        if (androidJoyInput != null) {
-            androidJoyInput.loadSettings(settings);
-        }
 
         if (settings.getFrameRate() > 0) {
             minFrameDuration = (long)(1000d / (double)settings.getFrameRate()); // ms
@@ -274,12 +270,12 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
 
     @Override
     public JoyInput getJoyInput() {
-        return androidJoyInput;
+        return androidInput.getJoyInput();
     }
 
     @Override
     public TouchInput getTouchInput() {
-        return androidInput;
+        return androidInput.getTouchInput();
     }
 
     @Override

+ 16 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java

@@ -110,6 +110,8 @@ public class ConstraintDefinitionIK extends ConstraintDefinition {
             }
         }
 
+        List<Transform> bestSolution = new ArrayList<Transform>(bones.size());
+        double bestSolutionDistance = Double.MAX_VALUE;
         BoneContext topBone = bones.get(0);
         for (int i = 0; i < iterations && distanceFromTarget > MIN_DISTANCE; ++i) {
             for (BoneContext boneContext : bones) {
@@ -150,6 +152,20 @@ public class ConstraintDefinitionIK extends ConstraintDefinition {
             DTransform topBoneTransform = new DTransform(constraintHelper.getTransform(topBone.getArmatureObjectOMA(), topBone.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD));
             Vector3d e = topBoneTransform.getTranslation().addLocal(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
             distanceFromTarget = e.distance(t);
+            
+            if(distanceFromTarget < bestSolutionDistance) {
+                bestSolutionDistance = distanceFromTarget;
+                bestSolution.clear();
+                for(BoneContext boneContext : bones) {
+                    bestSolution.add(constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD));
+                }
+            }
+        }
+        
+        // applying best solution
+        for(int i=0;i<bestSolution.size();++i) {
+            BoneContext boneContext = bones.get(i);
+            constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD, bestSolution.get(i));
         }
     }
 

+ 1 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java

@@ -195,7 +195,7 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
 
     /**
      * When set to true, the physics coordinates will be applied to the local
-     * translation of the Spatial instead of the world traslation.
+     * translation of the Spatial instead of the world translation.
      * @param applyPhysicsLocal
      */
     public void setApplyPhysicsLocal(boolean applyPhysicsLocal) {

+ 20 - 0
jme3-core/src/main/java/com/jme3/animation/Bone.java

@@ -313,6 +313,26 @@ public final class Bone implements Savable {
         return modelBindInverseScale;
     }
 
+    public Transform getModelBindInverseTransform() {
+        Transform t = new Transform();
+        t.setTranslation(modelBindInversePos);
+        t.setRotation(modelBindInverseRot);
+        if (modelBindInverseScale != null) {
+            t.setScale(modelBindInverseScale);
+        }
+        return t;
+    }
+    
+    public Transform getBindInverseTransform() {
+        Transform t = new Transform();
+        t.setTranslation(bindPos);
+        t.setRotation(bindRot);
+        if (bindScale != null) {
+            t.setScale(bindScale);
+        }
+        return t.invert();
+    }
+    
     /**    
      * @deprecated use {@link #getBindPosition()}
      */

+ 19 - 0
jme3-core/src/main/java/com/jme3/math/FastMath.java

@@ -902,6 +902,25 @@ final public class FastMath {
         return clamp(input, 0f, 1f);
     }
 
+    /**
+     * Determine if two floats are approximately equal.
+     * This takes into account the magnitude of the floats, since
+     * large numbers will have larger differences be close to each other.
+     * 
+     * Should return true for a=100000, b=100001, but false for a=10000, b=10001.
+     * 
+     * @param a The first float to compare
+     * @param b The second float to compare
+     * @return True if a and b are approximately equal, false otherwise.
+     */
+    public static boolean approximateEquals(float a, float b) {
+        if (a == b) {
+            return true;
+        } else {
+            return (abs(a - b) / Math.max(abs(a), abs(b))) <= 0.00001f;
+        }
+    }
+    
     /**
      * Converts a single precision (32 bit) floating point value
      * into half precision (16 bit).

+ 20 - 0
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -259,6 +259,26 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
         return store;
     }
 
+    public Matrix4f toTransformMatrix() {
+        Matrix4f trans = new Matrix4f();
+        trans.setTranslation(translation);
+        trans.setRotationQuaternion(rot);
+        trans.setScale(scale);
+        return trans;
+    }
+    
+    public void fromTransformMatrix(Matrix4f mat) {
+        translation.set(mat.toTranslationVector());
+        rot.set(mat.toRotationQuat());
+        scale.set(mat.toScaleVector());
+    }
+    
+    public Transform invert() {
+        Transform t = new Transform();
+        t.fromTransformMatrix(toTransformMatrix().invertLocal());
+        return t;
+    }
+    
     /**
      * Loads the identity.  Equal to translation=0,0,0 scale=1,1,1 rot=0,0,0,1.
      */

+ 13 - 1
jme3-core/src/main/java/com/jme3/renderer/Caps.java

@@ -337,7 +337,19 @@ public enum Caps {
      * <p>
      * Improves the quality of environment mapping.
      */
-    SeamlessCubemap;
+    SeamlessCubemap,
+    
+    /**
+     * Running with OpenGL 3.2+ core profile.
+     * 
+     * Compatibility features will not be available.
+     */
+    CoreProfile,
+    
+    /**
+     * GPU can provide and accept binary shaders.
+     */
+    BinaryShader;
 
     /**
      * Returns true if given the renderer capabilities, the texture

+ 2 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java

@@ -32,7 +32,6 @@
 package com.jme3.renderer.opengl;
 
 import java.nio.ByteBuffer;
-import java.nio.DoubleBuffer;
 import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
 import java.nio.ShortBuffer;
@@ -178,6 +177,8 @@ public interface GL {
 	public static final int GL_VERTEX_SHADER = 0x8B31;
 	public static final int GL_ZERO = 0x0;
 
+        public void resetStats();
+        
 	public void glActiveTexture(int texture);
 	public void glAttachShader(int program, int shader);
 	public void glBindBuffer(int target, int buffer);

+ 7 - 3
jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java

@@ -35,14 +35,18 @@ import java.nio.IntBuffer;
 
 /**
  * GL functions only available on vanilla desktop OpenGL 3.0.
- * 
+ *
  * @author Kirill Vainer
  */
 public interface GL3 extends GL2 {
-    
+
     public static final int GL_DEPTH_STENCIL_ATTACHMENT = 0x821A;
-    public static final int GL_GEOMETRY_SHADER=0x8DD9;
+    public static final int GL_GEOMETRY_SHADER = 0x8DD9;
+    public static final int GL_NUM_EXTENSIONS = 0x821D;
+    
     public void glBindFragDataLocation(int param1, int param2, String param3); /// GL3+
     public void glBindVertexArray(int param1); /// GL3+
+    public void glDeleteVertexArrays(IntBuffer arrays); /// GL3+
     public void glGenVertexArrays(IntBuffer param1); /// GL3+
+    public String glGetString(int param1, int param2); /// GL3+
 }

+ 23 - 3
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java

@@ -3,15 +3,17 @@ package com.jme3.renderer.opengl;
 import java.nio.ByteBuffer;
 import java.nio.IntBuffer;
 
-public class GLDebugDesktop extends GLDebugES implements GL2, GL3 {
+public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 {
 
     private final GL2 gl2;
     private final GL3 gl3;
+    private final GL4 gl4;
     
-    public GLDebugDesktop(GL gl, GLFbo glfbo) {
-        super(gl, glfbo);
+    public GLDebugDesktop(GL gl, GLExt glext, GLFbo glfbo) {
+        super(gl, glext, glfbo);
         this.gl2 = gl instanceof GL2 ? (GL2) gl : null;
         this.gl3 = gl instanceof GL3 ? (GL3) gl : null;
+        this.gl4 = gl instanceof GL4 ? (GL4) gl : null;
     }
     
     public void glAlphaFunc(int func, float ref) {
@@ -73,5 +75,23 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3 {
         gl3.glGenVertexArrays(param1);
         checkError();
     }
+    
+    @Override
+    public String glGetString(int param1, int param2) {
+        String result = gl3.glGetString(param1, param2);
+        checkError();
+        return result;
+    }
+
+    @Override
+    public void glDeleteVertexArrays(IntBuffer arrays) {
+        gl3.glDeleteVertexArrays(arrays);
+        checkError();
+    }
 
+    @Override
+    public void glPatchParameter(int count) {
+        gl4.glPatchParameter(count);
+        checkError();
+    }
 }

+ 8 - 6
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugES.java

@@ -10,14 +10,16 @@ public class GLDebugES extends GLDebug implements GL, GLFbo, GLExt {
     private final GLFbo glfbo;
     private final GLExt glext;
 
-    public GLDebugES(GL gl, GLFbo glfbo) {
+    public GLDebugES(GL gl, GLExt glext, GLFbo glfbo) {
         this.gl = gl;
-//        this.gl2 = gl instanceof GL2 ? (GL2) gl : null;
-//        this.gl3 = gl instanceof GL3 ? (GL3) gl : null;
+        this.glext = glext;
         this.glfbo = glfbo;
-        this.glext = glfbo instanceof GLExt ? (GLExt) glfbo : null;
     }
 
+    public void resetStats() {
+        gl.resetStats();
+    }
+    
     public void glActiveTexture(int texture) {
         gl.glActiveTexture(texture);
         checkError();
@@ -478,7 +480,7 @@ public class GLDebugES extends GLDebug implements GL, GLFbo, GLExt {
     }
 
     public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) {
-        glext.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+        glfbo.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
         checkError();
     }
 
@@ -525,7 +527,7 @@ public class GLDebugES extends GLDebug implements GL, GLFbo, GLExt {
     }
 
     public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) {
-        glext.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height);
+        glfbo.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height);
         checkError();
     }
 

+ 1 - 3
jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java

@@ -41,7 +41,7 @@ import java.nio.IntBuffer;
  * 
  * @author Kirill Vainer
  */
-public interface GLExt extends GLFbo {
+public interface GLExt {
 
         public static final int GL_ALREADY_SIGNALED = 0x911A;
 	public static final int GL_COMPRESSED_RGB8_ETC2 = 0x9274;
@@ -100,7 +100,6 @@ public interface GLExt extends GLFbo {
 	public static final int GL_UNSIGNED_INT_5_9_9_9_REV_EXT = 0x8C3E;
         public static final int GL_WAIT_FAILED = 0x911D;
 
-	public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter);
         public void glBufferData(int target, IntBuffer data, int usage);
         public void glBufferSubData(int target, long offset, IntBuffer data);
         public int glClientWaitSync(Object sync, int flags, long timeout);
@@ -110,7 +109,6 @@ public interface GLExt extends GLFbo {
 	public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount);
         public Object glFenceSync(int condition, int flags);
 	public void glGetMultisample(int pname, int index, FloatBuffer val);
-        public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height);
 	public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations);
 	public void glVertexAttribDivisorARB(int index, int divisor);
 }

+ 2 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GLFbo.java

@@ -83,6 +83,7 @@ public interface GLFbo {
     
     public void glBindFramebufferEXT(int param1, int param2);
     public void glBindRenderbufferEXT(int param1, int param2);
+    public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter);
     public int glCheckFramebufferStatusEXT(int param1);
     public void glDeleteFramebuffersEXT(IntBuffer param1);
     public void glDeleteRenderbuffersEXT(IntBuffer param1);
@@ -92,5 +93,5 @@ public interface GLFbo {
     public void glGenRenderbuffersEXT(IntBuffer param1);
     public void glGenerateMipmapEXT(int param1);
     public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4);
-    
+    public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height);
 }

+ 24 - 14
jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java

@@ -89,9 +89,11 @@ public final class GLImageFormats {
         GLImageFormat[][] formatToGL = new GLImageFormat[2][Image.Format.values().length];
         
         if (caps.contains(Caps.OpenGL20)) {
-            format(formatToGL, Format.Alpha8,           GL2.GL_ALPHA8,            GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8,       GL2.GL_LUMINANCE8,        GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            if (!caps.contains(Caps.CoreProfile)) {
+                format(formatToGL, Format.Alpha8,           GL2.GL_ALPHA8,            GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8,       GL2.GL_LUMINANCE8,        GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8Alpha8, GL2.GL_LUMINANCE8_ALPHA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            }
             format(formatToGL, Format.RGB8,             GL2.GL_RGB8,              GL.GL_RGB,             GL.GL_UNSIGNED_BYTE);
             format(formatToGL, Format.RGBA8,            GLExt.GL_RGBA8,           GL.GL_RGBA,            GL.GL_UNSIGNED_BYTE);
             format(formatToGL, Format.RGB565,           GL2.GL_RGB8,              GL.GL_RGB,             GL.GL_UNSIGNED_SHORT_5_6_5);
@@ -108,8 +110,10 @@ public final class GLImageFormats {
                 formatSrgb(formatToGL, Format.RGB565,           GLExt.GL_SRGB8_EXT,              GL.GL_RGB,             GL.GL_UNSIGNED_SHORT_5_6_5);
                 formatSrgb(formatToGL, Format.RGB5A1,           GLExt.GL_SRGB8_ALPHA8_EXT,       GL.GL_RGBA,            GL.GL_UNSIGNED_SHORT_5_5_5_1);
                 formatSrgb(formatToGL, Format.RGBA8,            GLExt.GL_SRGB8_ALPHA8_EXT,       GL.GL_RGBA,            GL.GL_UNSIGNED_BYTE);
-                formatSrgb(formatToGL, Format.Luminance8,       GLExt.GL_SLUMINANCE8_EXT,        GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
-                formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+                if (!caps.contains(Caps.CoreProfile)) {
+                    formatSrgb(formatToGL, Format.Luminance8,       GLExt.GL_SLUMINANCE8_EXT,        GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
+                    formatSrgb(formatToGL, Format.Luminance8Alpha8, GLExt.GL_SLUMINANCE8_ALPHA8_EXT, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+                }
                 formatSrgb(formatToGL, Format.BGR8,             GLExt.GL_SRGB8_EXT,              GL2.GL_BGR,            GL.GL_UNSIGNED_BYTE);
                 formatSrgb(formatToGL, Format.ABGR8,            GLExt.GL_SRGB8_ALPHA8_EXT,       GL.GL_RGBA,            GL2.GL_UNSIGNED_INT_8_8_8_8);
                 formatSrgb(formatToGL, Format.ARGB8,            GLExt.GL_SRGB8_ALPHA8_EXT,       GL2.GL_BGRA,           GL2.GL_UNSIGNED_INT_8_8_8_8);
@@ -124,16 +128,20 @@ public final class GLImageFormats {
             }
         } else if (caps.contains(Caps.Rgba8)) {
             // A more limited form of 32-bit RGBA. Only GL_RGBA8 is available.
-            format(formatToGL, Format.Alpha8,           GLExt.GL_RGBA8, GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8,       GLExt.GL_RGBA8, GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            if (!caps.contains(Caps.CoreProfile)) {
+                format(formatToGL, Format.Alpha8,           GLExt.GL_RGBA8, GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8,       GLExt.GL_RGBA8, GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8Alpha8, GLExt.GL_RGBA8, GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            }
             format(formatToGL, Format.RGB8,             GLExt.GL_RGBA8, GL.GL_RGB,             GL.GL_UNSIGNED_BYTE);
             format(formatToGL, Format.RGBA8,            GLExt.GL_RGBA8, GL.GL_RGBA,            GL.GL_UNSIGNED_BYTE);
         } else {
             // Actually, the internal format isn't used for OpenGL ES 2! This is the same as the above..
-            format(formatToGL, Format.Alpha8,           GL.GL_RGBA4,   GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8,       GL.GL_RGB565,  GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
-            format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4,   GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            if (!caps.contains(Caps.CoreProfile)) {
+                format(formatToGL, Format.Alpha8,           GL.GL_RGBA4,   GL.GL_ALPHA,           GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8,       GL.GL_RGB565,  GL.GL_LUMINANCE,       GL.GL_UNSIGNED_BYTE);
+                format(formatToGL, Format.Luminance8Alpha8, GL.GL_RGBA4,   GL.GL_LUMINANCE_ALPHA, GL.GL_UNSIGNED_BYTE);
+            }
             format(formatToGL, Format.RGB8,             GL.GL_RGB565,  GL.GL_RGB,             GL.GL_UNSIGNED_BYTE);
             format(formatToGL, Format.RGBA8,            GL.GL_RGBA4,   GL.GL_RGBA,            GL.GL_UNSIGNED_BYTE);
         }
@@ -145,9 +153,11 @@ public final class GLImageFormats {
         format(formatToGL, Format.RGB5A1, GL.GL_RGB5_A1, GL.GL_RGBA, GL.GL_UNSIGNED_SHORT_5_5_5_1);
         
         if (caps.contains(Caps.FloatTexture)) {
-            format(formatToGL, Format.Luminance16F,         GLExt.GL_LUMINANCE16F_ARB,       GL.GL_LUMINANCE,       GLExt.GL_HALF_FLOAT_ARB);
-            format(formatToGL, Format.Luminance32F,         GLExt.GL_LUMINANCE32F_ARB,       GL.GL_LUMINANCE,       GL.GL_FLOAT);
-            format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, GLExt.GL_HALF_FLOAT_ARB);
+            if (!caps.contains(Caps.CoreProfile)) {
+                format(formatToGL, Format.Luminance16F,         GLExt.GL_LUMINANCE16F_ARB,       GL.GL_LUMINANCE,       GLExt.GL_HALF_FLOAT_ARB);
+                format(formatToGL, Format.Luminance32F,         GLExt.GL_LUMINANCE32F_ARB,       GL.GL_LUMINANCE,       GL.GL_FLOAT);
+                format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, GLExt.GL_HALF_FLOAT_ARB);
+            }
             format(formatToGL, Format.RGB16F,               GLExt.GL_RGB16F_ARB,             GL.GL_RGB,             GLExt.GL_HALF_FLOAT_ARB);
             format(formatToGL, Format.RGB32F,               GLExt.GL_RGB32F_ARB,             GL.GL_RGB,             GL.GL_FLOAT);
             format(formatToGL, Format.RGBA16F,              GLExt.GL_RGBA16F_ARB,            GL.GL_RGBA,            GLExt.GL_HALF_FLOAT_ARB);

+ 78 - 88
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -56,6 +56,7 @@ import com.jme3.util.BufferUtils;
 import com.jme3.util.ListMap;
 import com.jme3.util.NativeObjectManager;
 import java.nio.*;
+import java.util.Arrays;
 import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.HashSet;
@@ -112,13 +113,13 @@ public class GLRenderer implements Renderer {
     private final GLFbo glfbo;
     private final TextureUtil texUtil;
     
-    public GLRenderer(GL gl, GLFbo glfbo) {
+    public GLRenderer(GL gl, GLExt glext, GLFbo glfbo) {
         this.gl = gl;
         this.gl2 = gl instanceof GL2 ? (GL2)gl : null;
         this.gl3 = gl instanceof GL3 ? (GL3)gl : null;
         this.gl4 = gl instanceof GL4 ? (GL4)gl : null;
         this.glfbo = glfbo;
-        this.glext = glfbo instanceof GLExt ? (GLExt)glfbo : null;
+        this.glext = glext;
         this.texUtil = new TextureUtil(gl, gl2, glext, context);
     }
 
@@ -137,10 +138,19 @@ public class GLRenderer implements Renderer {
         return limits;
     }
 
-    private static HashSet<String> loadExtensions(String extensions) {
+    private HashSet<String> loadExtensions() {
         HashSet<String> extensionSet = new HashSet<String>(64);
-        for (String extension : extensions.split(" ")) {
-            extensionSet.add(extension);
+        if (gl3 != null) {
+            // If OpenGL3+ is available, use the non-deprecated way
+            // of getting supported extensions.
+            gl3.glGetInteger(GL3.GL_NUM_EXTENSIONS, intBuf16);
+            int extensionCount = intBuf16.get(0);
+            for (int i = 0; i < extensionCount; i++) {
+                String extension = gl3.glGetString(GL.GL_EXTENSIONS, i);
+                extensionSet.add(extension);
+            }
+        } else {
+            extensionSet.addAll(Arrays.asList(gl.glGetString(GL.GL_EXTENSIONS).split(" ")));
         }
         return extensionSet;
     }
@@ -185,10 +195,12 @@ public class GLRenderer implements Renderer {
                         caps.add(Caps.OpenGL31);
                         if (oglVer >= 320) {
                             caps.add(Caps.OpenGL32);
-                        }if(oglVer>=330){
+                        }
+                        if (oglVer >= 330) {
                             caps.add(Caps.OpenGL33);
                             caps.add(Caps.GeometryShader);
-                        }if(oglVer>=400){
+                        }
+                        if (oglVer >= 400) {
                             caps.add(Caps.OpenGL40);
                             caps.add(Caps.TesselationShader);
                         }
@@ -243,7 +255,7 @@ public class GLRenderer implements Renderer {
     }
     
     private void loadCapabilitiesCommon() {
-        extensions = loadExtensions(gl.glGetString(GL.GL_EXTENSIONS));
+        extensions = loadExtensions();
         
         limits.put(Limits.VertexTextureUnits, getInteger(GL.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS));
         if (limits.get(Limits.VertexTextureUnits) > 0) {
@@ -251,7 +263,7 @@ public class GLRenderer implements Renderer {
         }
 
         limits.put(Limits.FragmentTextureUnits, getInteger(GL.GL_MAX_TEXTURE_IMAGE_UNITS));
-
+        
 //        gl.glGetInteger(GL.GL_MAX_VERTEX_UNIFORM_COMPONENTS, intBuf16);
 //        vertexUniforms = intBuf16.get(0);
 //        logger.log(Level.FINER, "Vertex Uniforms: {0}", vertexUniforms);
@@ -279,7 +291,7 @@ public class GLRenderer implements Renderer {
         
         // == texture format extensions ==
         
-        boolean hasFloatTexture = false;
+        boolean hasFloatTexture;
 
         hasFloatTexture = hasExtension("GL_OES_texture_half_float") &&
                           hasExtension("GL_OES_texture_float");
@@ -375,11 +387,11 @@ public class GLRenderer implements Renderer {
             caps.add(Caps.TextureFilterAnisotropic);
         }
 
-        if (hasExtension("GL_EXT_framebuffer_object")) {
+        if (hasExtension("GL_EXT_framebuffer_object") || gl3 != null) {
             caps.add(Caps.FrameBuffer);
             
-            limits.put(Limits.RenderBufferSize, getInteger(GLExt.GL_MAX_RENDERBUFFER_SIZE_EXT));
-            limits.put(Limits.FrameBufferAttachments, getInteger(GLExt.GL_MAX_COLOR_ATTACHMENTS_EXT));
+            limits.put(Limits.RenderBufferSize, getInteger(GLFbo.GL_MAX_RENDERBUFFER_SIZE_EXT));
+            limits.put(Limits.FrameBufferAttachments, getInteger(GLFbo.GL_MAX_COLOR_ATTACHMENTS_EXT));
             
             if (hasExtension("GL_EXT_framebuffer_blit")) {
                 caps.add(Caps.FrameBufferBlit);
@@ -434,21 +446,30 @@ public class GLRenderer implements Renderer {
             caps.add(Caps.SeamlessCubemap);
         }
         
-//        if (hasExtension("GL_ARB_get_program_binary")) {
-//            int binaryFormats = getInteger(GLExt.GL_NUM_PROGRAM_BINARY_FORMATS);
-//        }
+        if (caps.contains(Caps.OpenGL32) && !hasExtension("GL_ARB_compatibility")) {
+            caps.add(Caps.CoreProfile);
+        }
+        
+        if (hasExtension("GL_ARB_get_program_binary")) {
+            int binaryFormats = getInteger(GLExt.GL_NUM_PROGRAM_BINARY_FORMATS);
+            if (binaryFormats > 0) {
+                caps.add(Caps.BinaryShader);
+            }
+        }
         
         // Print context information
         logger.log(Level.INFO, "OpenGL Renderer Information\n" +
                                " * Vendor: {0}\n" +
                                " * Renderer: {1}\n" +
                                " * OpenGL Version: {2}\n" +
-                               " * GLSL Version: {3}",
+                               " * GLSL Version: {3}\n" +
+                               " * Profile: {4}",
                                new Object[]{ 
                                    gl.glGetString(GL.GL_VENDOR), 
                                    gl.glGetString(GL.GL_RENDERER),
                                    gl.glGetString(GL.GL_VERSION),
-                                   gl.glGetString(GL.GL_SHADING_LANGUAGE_VERSION)
+                                   gl.glGetString(GL.GL_SHADING_LANGUAGE_VERSION),
+                                   caps.contains(Caps.CoreProfile) ? "Core" : "Compatibility"
                                });
         
         // Print capabilities (if fine logging is enabled)
@@ -491,6 +512,20 @@ public class GLRenderer implements Renderer {
         
         // Initialize default state..
         gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
+        
+        if (caps.contains(Caps.CoreProfile)) {
+            // Core Profile requires VAO to be bound.
+            gl3.glGenVertexArrays(intBuf16);
+            int vaoId = intBuf16.get(0);
+            gl3.glBindVertexArray(vaoId);
+        } 
+        if (gl2 != null) {
+            gl2.glEnable(GL2.GL_VERTEX_PROGRAM_POINT_SIZE);
+            if (!caps.contains(Caps.CoreProfile)) {
+                gl2.glEnable(GL2.GL_POINT_SPRITE);
+                context.pointSprite = true;
+            }
+        }
     }
 
     public void invalidateState() {
@@ -610,31 +645,6 @@ public class GLRenderer implements Renderer {
             context.colorWriteEnabled = false;
         }
 
-        if (gl2 != null) {
-            if (state.isPointSprite() && !context.pointSprite) {
-                // Only enable/disable sprite
-                if (context.boundTextures[0] != null) {
-                    if (context.boundTextureUnit != 0) {
-                        gl.glActiveTexture(GL.GL_TEXTURE0);
-                        context.boundTextureUnit = 0;
-                    }
-                    gl2.glEnable(GL2.GL_POINT_SPRITE);
-                    gl2.glEnable(GL2.GL_VERTEX_PROGRAM_POINT_SIZE);
-                }
-                context.pointSprite = true;
-            } else if (!state.isPointSprite() && context.pointSprite) {
-                if (context.boundTextures[0] != null) {
-                    if (context.boundTextureUnit != 0) {
-                        gl.glActiveTexture(GL.GL_TEXTURE0);
-                        context.boundTextureUnit = 0;
-                    }
-                    gl2.glDisable(GL2.GL_POINT_SPRITE);
-                    gl2.glDisable(GL2.GL_VERTEX_PROGRAM_POINT_SIZE);
-                    context.pointSprite = false;
-                }
-            }
-        }
-
         if (state.isPolyOffset()) {
             if (!context.polyOffsetEnabled) {
                 gl.glEnable(GL.GL_POLYGON_OFFSET_FILL);
@@ -704,9 +714,6 @@ public class GLRenderer implements Renderer {
                     case AlphaAdditive:
                         gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
                         break;
-                    case Color:
-                        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR);
-                        break;
                     case Alpha:
                         gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
                         break;
@@ -719,6 +726,7 @@ public class GLRenderer implements Renderer {
                     case ModulateX2:
                         gl.glBlendFunc(GL.GL_DST_COLOR, GL.GL_SRC_COLOR);
                         break;
+                    case Color:
                     case Screen:
                         gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR);
                         break;       
@@ -862,6 +870,7 @@ public class GLRenderer implements Renderer {
 
     public void postFrame() {
         objManager.deleteUnused(this);
+        gl.resetStats();
     }
 
     /*********************************************************************\
@@ -1290,24 +1299,24 @@ public class GLRenderer implements Renderer {
             }
 
             if (src == null) {
-                glfbo.glBindFramebufferEXT(GLExt.GL_READ_FRAMEBUFFER_EXT, 0);
+                glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, 0);
                 srcX0 = vpX;
                 srcY0 = vpY;
                 srcX1 = vpX + vpW;
                 srcY1 = vpY + vpH;
             } else {
-                glfbo.glBindFramebufferEXT(GLExt.GL_READ_FRAMEBUFFER_EXT, src.getId());
+                glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, src.getId());
                 srcX1 = src.getWidth();
                 srcY1 = src.getHeight();
             }
             if (dst == null) {
-                glfbo.glBindFramebufferEXT(GLExt.GL_DRAW_FRAMEBUFFER_EXT, 0);
+                glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, 0);
                 dstX0 = vpX;
                 dstY0 = vpY;
                 dstX1 = vpX + vpW;
                 dstY1 = vpY + vpH;
             } else {
-                glfbo.glBindFramebufferEXT(GLExt.GL_DRAW_FRAMEBUFFER_EXT, dst.getId());
+                glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, dst.getId());
                 dstX1 = dst.getWidth();
                 dstY1 = dst.getHeight();
             }
@@ -1315,12 +1324,12 @@ public class GLRenderer implements Renderer {
             if (copyDepth) {
                 mask |= GL.GL_DEPTH_BUFFER_BIT;
             }
-            glext.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1,
+            glfbo.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1,
                     dstX0, dstY0, dstX1, dstY1, mask,
                     GL.GL_NEAREST);
 
 
-            glfbo.glBindFramebufferEXT(GLExt.GL_FRAMEBUFFER_EXT, prevFBO);
+            glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, prevFBO);
         } else {
             throw new RendererException("Framebuffer blitting not supported by the video hardware");
         }
@@ -1366,7 +1375,7 @@ public class GLRenderer implements Renderer {
         }
 
         if (context.boundRB != id) {
-            glfbo.glBindRenderbufferEXT(GLExt.GL_RENDERBUFFER_EXT, id);
+            glfbo.glBindRenderbufferEXT(GLFbo.GL_RENDERBUFFER_EXT, id);
             context.boundRB = id;
         }
 
@@ -1384,13 +1393,13 @@ public class GLRenderer implements Renderer {
             if (maxSamples < samples) {
                 samples = maxSamples;
             }
-            glext.glRenderbufferStorageMultisampleEXT(GLExt.GL_RENDERBUFFER_EXT,
+            glfbo.glRenderbufferStorageMultisampleEXT(GLFbo.GL_RENDERBUFFER_EXT,
                     samples,
                     glFmt.internalFormat,
                     fb.getWidth(),
                     fb.getHeight());
         } else {
-            glfbo.glRenderbufferStorageEXT(GLExt.GL_RENDERBUFFER_EXT,
+            glfbo.glRenderbufferStorageEXT(GLFbo.GL_RENDERBUFFER_EXT,
                     glFmt.internalFormat,
                     fb.getWidth(),
                     fb.getHeight());
@@ -1400,7 +1409,7 @@ public class GLRenderer implements Renderer {
     private int convertAttachmentSlot(int attachmentSlot) {
         // can also add support for stencil here
         if (attachmentSlot == FrameBuffer.SLOT_DEPTH) {
-            return GLExt.GL_DEPTH_ATTACHMENT_EXT;
+            return GLFbo.GL_DEPTH_ATTACHMENT_EXT;
         } else if (attachmentSlot == FrameBuffer.SLOT_DEPTH_STENCIL) {
             // NOTE: Using depth stencil format requires GL3, this is already
             // checked via render caps.
@@ -1409,7 +1418,7 @@ public class GLRenderer implements Renderer {
             throw new UnsupportedOperationException("Invalid FBO attachment slot: " + attachmentSlot);
         }
         
-        return GLExt.GL_COLOR_ATTACHMENT0_EXT + attachmentSlot;
+        return GLFbo.GL_COLOR_ATTACHMENT0_EXT + attachmentSlot;
     }
 
     public void updateRenderTexture(FrameBuffer fb, RenderBuffer rb) {
@@ -1427,7 +1436,7 @@ public class GLRenderer implements Renderer {
             setupTextureParams(tex);
         }
 
-        glfbo.glFramebufferTexture2DEXT(GLExt.GL_FRAMEBUFFER_EXT,
+        glfbo.glFramebufferTexture2DEXT(GLFbo.GL_FRAMEBUFFER_EXT,
                 convertAttachmentSlot(rb.getSlot()),
                 convertTextureType(tex.getType(), image.getMultiSamples(), rb.getFace()),
                 image.getId(),
@@ -1445,9 +1454,9 @@ public class GLRenderer implements Renderer {
             updateRenderTexture(fb, rb);
         }
         if (needAttach) {
-            glfbo.glFramebufferRenderbufferEXT(GLExt.GL_FRAMEBUFFER_EXT,
+            glfbo.glFramebufferRenderbufferEXT(GLFbo.GL_FRAMEBUFFER_EXT,
                     convertAttachmentSlot(rb.getSlot()),
-                    GLExt.GL_RENDERBUFFER_EXT,
+                    GLFbo.GL_RENDERBUFFER_EXT,
                     rb.getId());
         }
     }
@@ -1465,7 +1474,7 @@ public class GLRenderer implements Renderer {
         }
 
         if (context.boundFBO != id) {
-            glfbo.glBindFramebufferEXT(GLExt.GL_FRAMEBUFFER_EXT, id);
+            glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, id);
             // binding an FBO automatically sets draw buf to GL_COLOR_ATTACHMENT0
             context.boundDrawBuf = 0;
             context.boundFBO = id;
@@ -1545,7 +1554,7 @@ public class GLRenderer implements Renderer {
         if (fb == null) {
             // unbind any fbos
             if (context.boundFBO != 0) {
-                glfbo.glBindFramebufferEXT(GLExt.GL_FRAMEBUFFER_EXT, 0);
+                glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0);
                 statistics.onFrameBufferUse(null, true);
 
                 context.boundFBO = 0;
@@ -1577,7 +1586,7 @@ public class GLRenderer implements Renderer {
             setViewPort(0, 0, fb.getWidth(), fb.getHeight());
             
             if (context.boundFBO != fb.getId()) {
-                glfbo.glBindFramebufferEXT(GLExt.GL_FRAMEBUFFER_EXT, fb.getId());
+                glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, fb.getId());
                 statistics.onFrameBufferUse(fb, true);
 
                 context.boundFBO = fb.getId();
@@ -1617,7 +1626,7 @@ public class GLRenderer implements Renderer {
                     if (context.boundDrawBuf != 100 + fb.getNumColorBuffers()) {
                         intBuf16.clear();
                         for (int i = 0; i < fb.getNumColorBuffers(); i++) {
-                            intBuf16.put(GLExt.GL_COLOR_ATTACHMENT0_EXT + i);
+                            intBuf16.put(GLFbo.GL_COLOR_ATTACHMENT0_EXT + i);
                         }
 
                         intBuf16.flip();
@@ -1629,7 +1638,7 @@ public class GLRenderer implements Renderer {
                     // select this draw buffer
                     if (gl2 != null) {
                         if (context.boundDrawBuf != rb.getSlot()) {
-                            gl2.glDrawBuffer(GLExt.GL_COLOR_ATTACHMENT0_EXT + rb.getSlot());
+                            gl2.glDrawBuffer(GLFbo.GL_COLOR_ATTACHMENT0_EXT + rb.getSlot());
                             context.boundDrawBuf = rb.getSlot();
                         }
                     }
@@ -1658,7 +1667,7 @@ public class GLRenderer implements Renderer {
             setFrameBuffer(fb);
             if (gl2 != null) {
                 if (context.boundReadBuf != rb.getSlot()) {
-                    gl2.glReadBuffer(GLExt.GL_COLOR_ATTACHMENT0_EXT + rb.getSlot());
+                    gl2.glReadBuffer(GLFbo.GL_COLOR_ATTACHMENT0_EXT + rb.getSlot());
                     context.boundReadBuf = rb.getSlot();
                 }
             }
@@ -1682,7 +1691,7 @@ public class GLRenderer implements Renderer {
     public void deleteFrameBuffer(FrameBuffer fb) {
         if (fb.getId() != -1) {
             if (context.boundFBO == fb.getId()) {
-                glfbo.glBindFramebufferEXT(GLExt.GL_FRAMEBUFFER_EXT, 0);
+                glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0);
                 context.boundFBO = 0;
             }
 
@@ -2620,32 +2629,13 @@ public class GLRenderer implements Renderer {
             return;
         }
 
-        if (context.pointSprite && mesh.getMode() != Mode.Points) {
-            // XXX: Hack, disable point sprite mode if mesh not in point mode
-            if (context.boundTextures[0] != null) {
-                if (context.boundTextureUnit != 0) {
-                    gl.glActiveTexture(GL.GL_TEXTURE0);
-                    context.boundTextureUnit = 0;
-                }
-                if (gl2 != null) {
-                    gl2.glDisable(GL2.GL_POINT_SPRITE);
-                    gl2.glDisable(GL2.GL_VERTEX_PROGRAM_POINT_SIZE);
-                }
-                context.pointSprite = false;
-            }
-        }
 
-        if (gl2 != null) {
-            if (context.pointSize != mesh.getPointSize()) {
-                gl2.glPointSize(mesh.getPointSize());
-                context.pointSize = mesh.getPointSize();
-            }
-        }
         if (context.lineWidth != mesh.getLineWidth()) {
             gl.glLineWidth(mesh.getLineWidth());
             context.lineWidth = mesh.getLineWidth();
         }
-        if(gl4!=null && mesh.getMode().equals(Mode.Patch)){
+        
+        if (gl4 != null && mesh.getMode().equals(Mode.Patch)) {
             gl4.glPatchParameter(mesh.getPatchVertexCount());
         }
         statistics.onMeshDrawn(mesh, lod, count);

+ 122 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLTiming.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2009-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.renderer.opengl;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Map;
+
+public class GLTiming implements InvocationHandler {
+    
+    private final Object obj;
+    private final GLTimingState state;
+    
+    public GLTiming(Object obj, GLTimingState state) {
+        this.obj = obj;
+        this.state = state;
+    }
+    
+    public static Object createGLTiming(Object glInterface, GLTimingState state, Class<?> ... glInterfaceClasses) {
+        return Proxy.newProxyInstance(glInterface.getClass().getClassLoader(),
+                                      glInterfaceClasses, 
+                                      new GLTiming(glInterface, state));
+    }
+    
+    private static class CallTimingComparator implements Comparator<Map.Entry<String, Long>> {
+        @Override
+        public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
+            return (int) (o2.getValue() - o1.getValue());
+        }
+    }
+    
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+        String methodName = method.getName();
+        if (methodName.equals("resetStats")) {
+            if (state.lastPrintOutTime + 1000000000 <= System.nanoTime() && state.sampleCount > 0) {
+                state.timeSpentInGL /= state.sampleCount;
+                System.out.println("--- TOTAL TIME SPENT IN GL CALLS: " + (state.timeSpentInGL/1000) + "us");
+                
+                Map.Entry<String, Long>[] callTimes = new Map.Entry[state.callTiming.size()];
+                int i = 0;
+                for (Map.Entry<String, Long> callTime : state.callTiming.entrySet()) {
+                    callTimes[i++] = callTime;
+                }
+                Arrays.sort(callTimes, new CallTimingComparator());
+                int limit = 10;
+                for (Map.Entry<String, Long> callTime : callTimes) {
+                    long val = callTime.getValue() / state.sampleCount;
+                    String name = callTime.getKey();
+                    String pad = "                                     ".substring(0, 30 - name.length());
+                    System.out.println("\t" + callTime.getKey() + pad + (val/1000) + "us");
+                    if (limit-- == 0) break;
+                }
+                for (Map.Entry<String, Long> callTime : callTimes) {
+                    state.callTiming.put(callTime.getKey(), Long.valueOf(0));
+                }
+                
+                state.sampleCount = 0;
+                state.timeSpentInGL = 0;
+                state.lastPrintOutTime = System.nanoTime();
+            } else {
+                state.sampleCount++;
+            }
+            return null;
+        } else {
+            Long currentTimeObj = state.callTiming.get(methodName);
+            long currentTime = 0;
+            if (currentTimeObj != null) currentTime = currentTimeObj;
+            
+            
+            long startTime = System.nanoTime();
+            Object result = method.invoke(obj, args);
+            long delta = System.nanoTime() - startTime;
+            
+            currentTime += delta;
+            state.timeSpentInGL += delta;
+            
+            state.callTiming.put(methodName, currentTime);
+            
+            if (delta > 1000000 && !methodName.equals("glClear")) {
+                // More than 1ms
+                // Ignore glClear as it cannot be avoided.
+                System.out.println("GL call " + methodName + " took " + (delta/1000) + "us to execute!");
+            }
+            
+            return result;
+        }
+    }
+    
+}

+ 41 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLTimingState.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2009-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.renderer.opengl;
+
+import java.util.HashMap;
+
+public class GLTimingState {
+    long timeSpentInGL = 0;
+    int sampleCount = 0;
+    long lastPrintOutTime = 0;
+    final HashMap<String, Long> callTiming = new HashMap<String, Long>();
+}

+ 3 - 4
jme3-core/src/main/java/com/jme3/renderer/opengl/GLTracer.java

@@ -64,6 +64,7 @@ public final class GLTracer implements InvocationHandler {
         noEnumArgs("glScissor", 0, 1, 2, 3);
         noEnumArgs("glClear", 0);
         noEnumArgs("glGetInteger", 1);
+        noEnumArgs("glGetString", 1);
         
         noEnumArgs("glBindTexture", 1);
         noEnumArgs("glPixelStorei", 1);
@@ -95,8 +96,6 @@ public final class GLTracer implements InvocationHandler {
         noEnumArgs("glFramebufferTexture2DEXT", 3, 4);
         noEnumArgs("glBlitFramebufferEXT", 0, 1, 2, 3, 4, 5, 6, 7, 8);
         
-        
-        
         noEnumArgs("glCreateProgram", -1);
         noEnumArgs("glCreateShader", -1);
         noEnumArgs("glShaderSource", 0);
@@ -155,7 +154,7 @@ public final class GLTracer implements InvocationHandler {
      * @return A tracer that implements the given interface
      */
     public static Object createGlesTracer(Object glInterface, Class<?> glInterfaceClass) {
-        IntMap<String> constMap = generateConstantMap(GL.class, GLExt.class);
+        IntMap<String> constMap = generateConstantMap(GL.class, GLFbo.class, GLExt.class);
         return Proxy.newProxyInstance(glInterface.getClass().getClassLoader(),
                                       new Class<?>[] { glInterfaceClass }, 
                                       new GLTracer(glInterface, constMap));
@@ -169,7 +168,7 @@ public final class GLTracer implements InvocationHandler {
      * @return A tracer that implements the given interface
      */
     public static Object createDesktopGlTracer(Object glInterface, Class<?> ... glInterfaceClasses) {
-        IntMap<String> constMap = generateConstantMap(GL2.class, GLExt.class);
+        IntMap<String> constMap = generateConstantMap(GL2.class, GL3.class, GL4.class, GLFbo.class, GLExt.class);
         return Proxy.newProxyInstance(glInterface.getClass().getClassLoader(),
                                       glInterfaceClasses, 
                                       new GLTracer(glInterface, constMap));

+ 1 - 0
jme3-core/src/main/java/com/jme3/shader/Glsl100ShaderGenerator.java

@@ -127,6 +127,7 @@ public class Glsl100ShaderGenerator extends ShaderGenerator {
             unIndent();
             startCondition(shaderNode.getCondition(), source);
             source.append(nodeSource);
+            source.append("\n");
             endCondition(shaderNode.getCondition(), source);
             indent();
         }

+ 14 - 10
jme3-core/src/main/java/com/jme3/texture/Image.java

@@ -421,7 +421,8 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
     
     /**
      * @return True if the image needs to have mipmaps generated
-     * for it (as requested by the texture).
+     * for it (as requested by the texture). This stays true even
+     * after mipmaps have been generated.
      */
     public boolean isGeneratedMipmapsRequired() {
         return needGeneratedMips;
@@ -434,8 +435,9 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
     @Override
     public void setUpdateNeeded() {
         super.setUpdateNeeded();
-        if (!isGeneratedMipmapsRequired() && !hasMipmaps()) {
-            setNeedGeneratedMipmaps();
+        if (isGeneratedMipmapsRequired() && !hasMipmaps()) {
+            // Mipmaps are no longer valid, since the image was changed.
+            setMipmapsGenerated(false);
         }
     }
     
@@ -527,11 +529,13 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
         
         this();
 
-        if (mipMapSizes != null && mipMapSizes.length <= 1) {
-            mipMapSizes = null;
-        } else {
-            needGeneratedMips = false;
-            mipsWereGenerated = true;
+        if (mipMapSizes != null) {
+            if (mipMapSizes.length <= 1) {
+                mipMapSizes = null;
+            } else {
+                needGeneratedMips = false;
+                mipsWereGenerated = true;
+            }
         }
 
         setFormat(format);
@@ -787,8 +791,8 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
              needGeneratedMips = false;
              mipsWereGenerated = false;
         } else {
-             needGeneratedMips = false;
-             mipsWereGenerated = true;
+             needGeneratedMips = true;
+             mipsWereGenerated = false;
         }
 
         setUpdateNeeded();

+ 11 - 9
jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.frag

@@ -72,17 +72,19 @@ uniform float m_Shininess;
 #endif
 
 void main(){
-    #ifdef NORMALMAP   
-        mat3 tbnMat = mat3(normalize(vTangent.xyz) , normalize(vBinormal.xyz) , normalize(vNormal.xyz));
+    #if !defined(VERTEX_LIGHTING)
+        #if defined(NORMALMAP)
+            mat3 tbnMat = mat3(normalize(vTangent.xyz) , normalize(vBinormal.xyz) , normalize(vNormal.xyz));
 
-        if (!gl_FrontFacing)
-        {
-            tbnMat[2] = -tbnMat[2];
-        }
+            if (!gl_FrontFacing)
+            {
+                tbnMat[2] = -tbnMat[2];
+            }
 
-        vec3 viewDir = normalize(-vPos.xyz * tbnMat);
-    #else
-        vec3 viewDir = normalize(-vPos.xyz);
+            vec3 viewDir = normalize(-vPos.xyz * tbnMat);
+        #else
+            vec3 viewDir = normalize(-vPos.xyz);
+        #endif
     #endif
 
     vec2 newTexCoord;

+ 8 - 5
jme3-core/src/main/resources/Common/MatDefs/Light/SPLighting.vert

@@ -45,6 +45,9 @@ attribute vec3 inNormal;
 #else
     varying vec3 specularAccum;
     varying vec4 diffuseAccum;
+    #ifdef COLORRAMP
+      uniform sampler2D m_ColorRamp;
+    #endif
 #endif
 
 #ifdef USE_REFLECTION
@@ -160,14 +163,14 @@ void main(){
             #if __VERSION__ >= 110
             }
             #endif
-            vec2 v = computeLighting(wvNormal, viewDir, lightDir.xyz, lightDir.w  * spotFallOff, m_Shininess);
+            vec2 light = computeLighting(wvNormal, viewDir, lightDir.xyz, lightDir.w  * spotFallOff, m_Shininess);
 
             #ifdef COLORRAMP
-                diffuseAccum  += texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb * diffuseColor;
-                specularAccum += texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb * specularColor;
+                diffuseAccum.rgb  += texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb * diffuseColor.rgb;
+                specularAccum.rgb += texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb * specularColor;
             #else
-                diffuseAccum  += v.x * diffuseColor;
-                specularAccum += v.y * specularColor;
+                diffuseAccum.rgb  += light.x * diffuseColor.rgb;
+                specularAccum.rgb += light.y * specularColor;
             #endif
         }
     #endif

+ 13 - 6
jme3-core/src/main/resources/joystick-mapping.properties

@@ -6,7 +6,7 @@
 #   the new name as it will be reported through the Joystick
 #   interface.
 #
-#   Keys with spaces in them should have those spaces escaped. 
+#   Keys with spaces in them should have those spaces escaped.
 #   Values do not need their spaces escaped.  For example:
 #
 #   Some\ Joystick.0=3
@@ -37,22 +37,29 @@ Controller\ (Xbox\ 360\ Wireless\ Receiver\ for\ Windows).ry=rz
 # requires custom code to support trigger buttons but this
 # keeps it from confusing the .rx mapping.
 Controller\ (Xbox\ 360\ Wireless\ Receiver\ for\ Windows).z=trigger
- 
+
 # Xbox 360 Controller (copied from wireless version)
 Controller\ (XBOX\ 360\ For\ Windows).0=2
 Controller\ (XBOX\ 360\ For\ Windows).1=1
 Controller\ (XBOX\ 360\ For\ Windows).2=3
 Controller\ (XBOX\ 360\ For\ Windows).3=0
- 
+
 Controller\ (XBOX\ 360\ For\ Windows).6=8
 Controller\ (XBOX\ 360\ For\ Windows).7=9
- 
+
 Controller\ (XBOX\ 360\ For\ Windows).8=10
 Controller\ (XBOX\ 360\ For\ Windows).9=11
- 
+
 Controller\ (XBOX\ 360\ For\ Windows).rx=z
 Controller\ (XBOX\ 360\ For\ Windows).ry=rz
- 
+
 # requires custom code to support trigger buttons but this
 # keeps it from confusing the .rx mapping.
 Controller\ (XBOX\ 360\ For\ Windows).z=trigger
+
+# XBOX 360 Controller connected to Android using
+# the USB dongle
+Xbox\ 360\ Wireless\ Receiver.AXIS_RX=z
+Xbox\ 360\ Wireless\ Receiver.AXIS_RY=rz
+Xbox\ 360\ Wireless\ Receiver.z=AXIS_RX
+Xbox\ 360\ Wireless\ Receiver.rz=AXIS_RY

+ 7 - 5
jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java

@@ -569,10 +569,15 @@ public class J3MLoader implements AssetLoader {
 
     public Object load(AssetInfo info) throws IOException {       
         this.assetManager = info.getManager();
-
+        
         InputStream in = info.openStream();        
         try {
-            key = info.getKey();            
+            key = info.getKey();
+            if (key.getExtension().equals("j3m") && !(key instanceof MaterialKey)) {
+                throw new IOException("Material instances must be loaded via MaterialKey");
+            } else if (key.getExtension().equals("j3md") && key instanceof MaterialKey) {
+                throw new IOException("Material definitions must be loaded via AssetKey");
+            }
             loadFromRoot(BlockLanguageParser.parse(in));
         } finally {
             if (in != null){
@@ -581,9 +586,6 @@ public class J3MLoader implements AssetLoader {
         }
         
         if (material != null){
-            if (!(info.getKey() instanceof MaterialKey)){
-                throw new IOException("Material instances must be loaded via MaterialKey");
-            }
             // material implementation
             return material;
         }else{

+ 12 - 11
jme3-core/src/plugins/java/com/jme3/texture/plugins/DXTFlipper.java

@@ -242,21 +242,12 @@ public class DXTFlipper {
                 img.position(blockByteOffset);
                 img.limit(blockByteOffset + bpb);
 
-                img.get(colorBlock);
-                if (type == 4 || type == 5)
-                    flipDXT5Block(colorBlock, h);
-                else
-                    flipDXT1orDXTA3Block(colorBlock, h);
-
-                // write block (no need to flip block indexes, only pixels
-                // inside block
-                retImg.put(colorBlock);
-
                 if (alphaBlock != null){
                     img.get(alphaBlock);
                     switch (type){
                         case 2:
-                            flipDXT3Block(alphaBlock, h); break;
+                            flipDXT3Block(alphaBlock, h); 
+                            break;
                         case 3:
                         case 4: 
                             flipDXT5Block(alphaBlock, h);
@@ -264,6 +255,16 @@ public class DXTFlipper {
                     }
                     retImg.put(alphaBlock);
                 }
+                
+                img.get(colorBlock);
+                if (type == 4 || type == 5)
+                    flipDXT5Block(colorBlock, h);
+                else
+                    flipDXT1orDXTA3Block(colorBlock, h);
+
+                // write block (no need to flip block indexes, only pixels
+                // inside block
+                retImg.put(colorBlock);
             }
             retImg.rewind();
         }else if (h >= 4){

+ 2 - 2
jme3-effects/src/main/java/com/jme3/post/ssao/SSAOFilter.java

@@ -162,8 +162,8 @@ public class SSAOFilter extends Filter {
         };
 
         ssaoPass.init(renderManager.getRenderer(), (int) (screenWidth / downSampleFactor), (int) (screenHeight / downSampleFactor), Format.RGBA8, Format.Depth, 1, ssaoMat);
-        ssaoPass.getRenderedTexture().setMinFilter(Texture.MinFilter.Trilinear);
-        ssaoPass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
+//        ssaoPass.getRenderedTexture().setMinFilter(Texture.MinFilter.Trilinear);
+//        ssaoPass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
         postRenderPasses.add(ssaoPass);
         material = new Material(manager, "Common/MatDefs/SSAO/ssaoBlur.j3md");
         material.setTexture("SSAOMap", ssaoPass.getRenderedTexture());

+ 1 - 5
jme3-effects/src/main/resources/Common/MatDefs/Water/Water.frag

@@ -413,10 +413,6 @@ void main(){
     // to calculate the derivatives for all these pixels by using step()!
     // That way we won't get pixels around the edges of the terrain,
     // Where the derivatives are undefined
-    if(position.y > level){
-            color = color2;
-    }
-
-    gl_FragColor = vec4(color,1.0);
+    gl_FragColor = vec4(mix(color, color2, step(level, position.y)), 1.0);
     
 }

+ 1 - 0
jme3-effects/src/main/resources/Common/MatDefs/Water/Water.j3md

@@ -77,6 +77,7 @@ MaterialDef Advanced Water {
         FragmentShader GLSL120 : Common/MatDefs/Water/Water.frag
 
         WorldParameters {
+            ViewProjectionMatrixInverse
         }
         Defines {
             ENABLE_RIPPLES : UseRipples

+ 4 - 0
jme3-ios/src/main/java/com/jme3/asset/IOS.cfg

@@ -2,3 +2,7 @@ INCLUDE com/jme3/asset/General.cfg
 
 # IOS specific loaders
 LOADER com.jme3.system.ios.IosImageLoader : jpg, bmp, gif, png, jpeg
+LOADER com.jme3.material.plugins.J3MLoader : j3m, j3md
+LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib
+LOADER com.jme3.export.binary.BinaryImporter : j3o
+LOADER com.jme3.font.plugins.BitmapFontLoader : fnt

+ 0 - 1054
jme3-ios/src/main/java/com/jme3/audio/android/AL.java

@@ -1,1054 +0,0 @@
-package com.jme3.audio.android;
-
-/**
- *
- * @author iwgeric
- */
-public class AL {
-
-
-
-    /* ********** */
-    /* FROM ALC.h */
-    /* ********** */
-
-//    typedef struct ALCdevice_struct ALCdevice;
-//    typedef struct ALCcontext_struct ALCcontext;
-
-
-    /**
-     * No error
-     */
-    static final int ALC_NO_ERROR = 0;
-
-    /**
-     * No device
-     */
-    static final int ALC_INVALID_DEVICE = 0xA001;
-
-    /**
-     * invalid context ID
-     */
-    static final int ALC_INVALID_CONTEXT = 0xA002;
-
-    /**
-     * bad enum
-     */
-    static final int ALC_INVALID_ENUM = 0xA003;
-
-    /**
-     * bad value
-     */
-    static final int ALC_INVALID_VALUE = 0xA004;
-
-    /**
-     * Out of memory.
-     */
-    static final int ALC_OUT_OF_MEMORY = 0xA005;
-
-
-    /**
-     * The Specifier string for default device
-     */
-    static final int ALC_DEFAULT_DEVICE_SPECIFIER = 0x1004;
-    static final int ALC_DEVICE_SPECIFIER = 0x1005;
-    static final int ALC_EXTENSIONS = 0x1006;
-
-    static final int ALC_MAJOR_VERSION = 0x1000;
-    static final int ALC_MINOR_VERSION = 0x1001;
-
-    static final int ALC_ATTRIBUTES_SIZE = 0x1002;
-    static final int ALC_ALL_ATTRIBUTES = 0x1003;
-
-
-    /**
-     * Capture extension
-     */
-    static final int ALC_EXT_CAPTURE = 1;
-    static final int ALC_CAPTURE_DEVICE_SPECIFIER = 0x310;
-    static final int ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER = 0x311;
-    static final int ALC_CAPTURE_SAMPLES = 0x312;
-
-
-    /**
-     * ALC_ENUMERATE_ALL_EXT enums
-     */
-    static final int ALC_ENUMERATE_ALL_EXT = 1;
-    static final int ALC_DEFAULT_ALL_DEVICES_SPECIFIER = 0x1012;
-    static final int ALC_ALL_DEVICES_SPECIFIER = 0x1013;
-
-
-    /* ********** */
-    /* FROM AL.h */
-    /* ********** */
-
-/** Boolean False. */
-    static final int AL_FALSE = 0;
-
-/** Boolean True. */
-    static final int AL_TRUE = 1;
-
-/* "no distance model" or "no buffer" */
-    static final int AL_NONE = 0;
-
-/** Indicate Source has relative coordinates. */
-    static final int AL_SOURCE_RELATIVE = 0x202;
-
-
-
-/**
- * Directional source, inner cone angle, in degrees.
- * Range:    [0-360]
- * Default:  360
- */
-    static final int AL_CONE_INNER_ANGLE = 0x1001;
-
-/**
- * Directional source, outer cone angle, in degrees.
- * Range:    [0-360]
- * Default:  360
- */
-    static final int AL_CONE_OUTER_ANGLE = 0x1002;
-
-/**
- * Specify the pitch to be applied at source.
- * Range:   [0.5-2.0]
- * Default: 1.0
- */
-    static final int AL_PITCH = 0x1003;
-
-/**
- * Specify the current location in three dimensional space.
- * OpenAL, like OpenGL, uses a right handed coordinate system,
- *  where in a frontal default view X (thumb) points right,
- *  Y points up (index finger), and Z points towards the
- *  viewer/camera (middle finger).
- * To switch from a left handed coordinate system, flip the
- *  sign on the Z coordinate.
- * Listener position is always in the world coordinate system.
- */
-    static final int AL_POSITION = 0x1004;
-
-/** Specify the current direction. */
-    static final int AL_DIRECTION = 0x1005;
-
-/** Specify the current velocity in three dimensional space. */
-    static final int AL_VELOCITY = 0x1006;
-
-/**
- * Indicate whether source is looping.
- * Type: ALboolean?
- * Range:   [AL_TRUE, AL_FALSE]
- * Default: FALSE.
- */
-    static final int AL_LOOPING = 0x1007;
-
-/**
- * Indicate the buffer to provide sound samples.
- * Type: ALuint.
- * Range: any valid Buffer id.
- */
-    static final int AL_BUFFER = 0x1009;
-
-/**
- * Indicate the gain (volume amplification) applied.
- * Type:   ALfloat.
- * Range:  ]0.0-  ]
- * A value of 1.0 means un-attenuated/unchanged.
- * Each division by 2 equals an attenuation of -6dB.
- * Each multiplicaton with 2 equals an amplification of +6dB.
- * A value of 0.0 is meaningless with respect to a logarithmic
- *  scale; it is interpreted as zero volume - the channel
- *  is effectively disabled.
- */
-    static final int AL_GAIN = 0x100A;
-
-/*
- * Indicate minimum source attenuation
- * Type: ALfloat
- * Range:  [0.0 - 1.0]
- *
- * Logarthmic
- */
-    static final int AL_MIN_GAIN = 0x100D;
-
-/**
- * Indicate maximum source attenuation
- * Type: ALfloat
- * Range:  [0.0 - 1.0]
- *
- * Logarthmic
- */
-    static final int AL_MAX_GAIN = 0x100E;
-
-/**
- * Indicate listener orientation.
- *
- * at/up
- */
-    static final int AL_ORIENTATION = 0x100F;
-
-/**
- * Source state information.
- */
-    static final int AL_SOURCE_STATE = 0x1010;
-    static final int AL_INITIAL = 0x1011;
-    static final int AL_PLAYING = 0x1012;
-    static final int AL_PAUSED = 0x1013;
-    static final int AL_STOPPED = 0x1014;
-
-/**
- * Buffer Queue params
- */
-    static final int AL_BUFFERS_QUEUED = 0x1015;
-    static final int AL_BUFFERS_PROCESSED = 0x1016;
-
-/**
- * Source buffer position information
- */
-    static final int AL_SEC_OFFSET = 0x1024;
-    static final int AL_SAMPLE_OFFSET = 0x1025;
-    static final int AL_BYTE_OFFSET = 0x1026;
-
-/*
- * Source type (Static, Streaming or undetermined)
- * Source is Static if a Buffer has been attached using AL_BUFFER
- * Source is Streaming if one or more Buffers have been attached using alSourceQueueBuffers
- * Source is undetermined when it has the NULL buffer attached
- */
-    static final int AL_SOURCE_TYPE = 0x1027;
-    static final int AL_STATIC = 0x1028;
-    static final int AL_STREAMING = 0x1029;
-    static final int AL_UNDETERMINED = 0x1030;
-
-/** Sound samples: format specifier. */
-    static final int AL_FORMAT_MONO8 = 0x1100;
-    static final int AL_FORMAT_MONO16 = 0x1101;
-    static final int AL_FORMAT_STEREO8 = 0x1102;
-    static final int AL_FORMAT_STEREO16 = 0x1103;
-
-/**
- * source specific reference distance
- * Type: ALfloat
- * Range:  0.0 - +inf
- *
- * At 0.0, no distance attenuation occurs.  Default is
- * 1.0.
- */
-    static final int AL_REFERENCE_DISTANCE = 0x1020;
-
-/**
- * source specific rolloff factor
- * Type: ALfloat
- * Range:  0.0 - +inf
- *
- */
-    static final int AL_ROLLOFF_FACTOR = 0x1021;
-
-/**
- * Directional source, outer cone gain.
- *
- * Default:  0.0
- * Range:    [0.0 - 1.0]
- * Logarithmic
- */
-    static final int AL_CONE_OUTER_GAIN = 0x1022;
-
-/**
- * Indicate distance above which sources are not
- * attenuated using the inverse clamped distance model.
- *
- * Default: +inf
- * Type: ALfloat
- * Range:  0.0 - +inf
- */
-    static final int AL_MAX_DISTANCE = 0x1023;
-
-/**
- * Sound samples: frequency, in units of Hertz [Hz].
- * This is the number of samples per second. Half of the
- *  sample frequency marks the maximum significant
- *  frequency component.
- */
-    static final int AL_FREQUENCY = 0x2001;
-    static final int AL_BITS = 0x2002;
-    static final int AL_CHANNELS = 0x2003;
-    static final int AL_SIZE = 0x2004;
-
-/**
- * Buffer state.
- *
- * Not supported for public use (yet).
- */
-    static final int AL_UNUSED = 0x2010;
-    static final int AL_PENDING = 0x2011;
-    static final int AL_PROCESSED = 0x2012;
-
-
-/** Errors: No Error. */
-    static final int AL_NO_ERROR = 0;
-
-/**
- * Invalid Name paramater passed to AL call.
- */
-    static final int AL_INVALID_NAME = 0xA001;
-
-/**
- * Invalid parameter passed to AL call.
- */
-    static final int AL_INVALID_ENUM = 0xA002;
-
-/**
- * Invalid enum parameter value.
- */
-    static final int AL_INVALID_VALUE = 0xA003;
-
-/**
- * Illegal call.
- */
-    static final int AL_INVALID_OPERATION = 0xA004;
-
-
-/**
- * No mojo.
- */
-    static final int AL_OUT_OF_MEMORY = 0xA005;
-
-
-/** Context strings: Vendor Name. */
-    static final int AL_VENDOR = 0xB001;
-    static final int AL_VERSION = 0xB002;
-    static final int AL_RENDERER = 0xB003;
-    static final int AL_EXTENSIONS = 0xB004;
-
-/** Global tweakage. */
-
-/**
- * Doppler scale.  Default 1.0
- */
-    static final int AL_DOPPLER_FACTOR = 0xC000;
-
-/**
- * Tweaks speed of propagation.
- */
-    static final int AL_DOPPLER_VELOCITY = 0xC001;
-
-/**
- * Speed of Sound in units per second
- */
-    static final int AL_SPEED_OF_SOUND = 0xC003;
-
-/**
- * Distance models
- *
- * used in conjunction with DistanceModel
- *
- * implicit: NONE, which disances distance attenuation.
- */
-    static final int AL_DISTANCE_MODEL = 0xD000;
-    static final int AL_INVERSE_DISTANCE = 0xD001;
-    static final int AL_INVERSE_DISTANCE_CLAMPED = 0xD002;
-    static final int AL_LINEAR_DISTANCE = 0xD003;
-    static final int AL_LINEAR_DISTANCE_CLAMPED = 0xD004;
-    static final int AL_EXPONENT_DISTANCE = 0xD005;
-    static final int AL_EXPONENT_DISTANCE_CLAMPED = 0xD006;
-
-    /* ********** */
-    /* FROM efx.h */
-    /* ********** */
-
-    static final String ALC_EXT_EFX_NAME = "ALC_EXT_EFX";
-
-    static final int ALC_EFX_MAJOR_VERSION = 0x20001;
-    static final int ALC_EFX_MINOR_VERSION = 0x20002;
-    static final int ALC_MAX_AUXILIARY_SENDS  = 0x20003;
-
-
-///* Listener properties. */
-//#define AL_METERS_PER_UNIT                       0x20004
-//
-///* Source properties. */
-    static final int AL_DIRECT_FILTER = 0x20005;
-    static final int AL_AUXILIARY_SEND_FILTER = 0x20006;
-//#define AL_AIR_ABSORPTION_FACTOR                 0x20007
-//#define AL_ROOM_ROLLOFF_FACTOR                   0x20008
-//#define AL_CONE_OUTER_GAINHF                     0x20009
-    static final int AL_DIRECT_FILTER_GAINHF_AUTO = 0x2000A;
-//#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO       0x2000B
-//#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO     0x2000C
-//
-//
-///* Effect properties. */
-//
-///* Reverb effect parameters */
-    static final int AL_REVERB_DENSITY = 0x0001;
-    static final int AL_REVERB_DIFFUSION = 0x0002;
-    static final int AL_REVERB_GAIN = 0x0003;
-    static final int AL_REVERB_GAINHF = 0x0004;
-    static final int AL_REVERB_DECAY_TIME = 0x0005;
-    static final int AL_REVERB_DECAY_HFRATIO = 0x0006;
-    static final int AL_REVERB_REFLECTIONS_GAIN = 0x0007;
-    static final int AL_REVERB_REFLECTIONS_DELAY = 0x0008;
-    static final int AL_REVERB_LATE_REVERB_GAIN = 0x0009;
-    static final int AL_REVERB_LATE_REVERB_DELAY = 0x000A;
-    static final int AL_REVERB_AIR_ABSORPTION_GAINHF = 0x000B;
-    static final int AL_REVERB_ROOM_ROLLOFF_FACTOR = 0x000C;
-    static final int AL_REVERB_DECAY_HFLIMIT = 0x000D;
-
-///* EAX Reverb effect parameters */
-//#define AL_EAXREVERB_DENSITY                     0x0001
-//#define AL_EAXREVERB_DIFFUSION                   0x0002
-//#define AL_EAXREVERB_GAIN                        0x0003
-//#define AL_EAXREVERB_GAINHF                      0x0004
-//#define AL_EAXREVERB_GAINLF                      0x0005
-//#define AL_EAXREVERB_DECAY_TIME                  0x0006
-//#define AL_EAXREVERB_DECAY_HFRATIO               0x0007
-//#define AL_EAXREVERB_DECAY_LFRATIO               0x0008
-//#define AL_EAXREVERB_REFLECTIONS_GAIN            0x0009
-//#define AL_EAXREVERB_REFLECTIONS_DELAY           0x000A
-//#define AL_EAXREVERB_REFLECTIONS_PAN             0x000B
-//#define AL_EAXREVERB_LATE_REVERB_GAIN            0x000C
-//#define AL_EAXREVERB_LATE_REVERB_DELAY           0x000D
-//#define AL_EAXREVERB_LATE_REVERB_PAN             0x000E
-//#define AL_EAXREVERB_ECHO_TIME                   0x000F
-//#define AL_EAXREVERB_ECHO_DEPTH                  0x0010
-//#define AL_EAXREVERB_MODULATION_TIME             0x0011
-//#define AL_EAXREVERB_MODULATION_DEPTH            0x0012
-//#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF       0x0013
-//#define AL_EAXREVERB_HFREFERENCE                 0x0014
-//#define AL_EAXREVERB_LFREFERENCE                 0x0015
-//#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR         0x0016
-//#define AL_EAXREVERB_DECAY_HFLIMIT               0x0017
-//
-///* Chorus effect parameters */
-//#define AL_CHORUS_WAVEFORM                       0x0001
-//#define AL_CHORUS_PHASE                          0x0002
-//#define AL_CHORUS_RATE                           0x0003
-//#define AL_CHORUS_DEPTH                          0x0004
-//#define AL_CHORUS_FEEDBACK                       0x0005
-//#define AL_CHORUS_DELAY                          0x0006
-//
-///* Distortion effect parameters */
-//#define AL_DISTORTION_EDGE                       0x0001
-//#define AL_DISTORTION_GAIN                       0x0002
-//#define AL_DISTORTION_LOWPASS_CUTOFF             0x0003
-//#define AL_DISTORTION_EQCENTER                   0x0004
-//#define AL_DISTORTION_EQBANDWIDTH                0x0005
-//
-///* Echo effect parameters */
-//#define AL_ECHO_DELAY                            0x0001
-//#define AL_ECHO_LRDELAY                          0x0002
-//#define AL_ECHO_DAMPING                          0x0003
-//#define AL_ECHO_FEEDBACK                         0x0004
-//#define AL_ECHO_SPREAD                           0x0005
-//
-///* Flanger effect parameters */
-//#define AL_FLANGER_WAVEFORM                      0x0001
-//#define AL_FLANGER_PHASE                         0x0002
-//#define AL_FLANGER_RATE                          0x0003
-//#define AL_FLANGER_DEPTH                         0x0004
-//#define AL_FLANGER_FEEDBACK                      0x0005
-//#define AL_FLANGER_DELAY                         0x0006
-//
-///* Frequency shifter effect parameters */
-//#define AL_FREQUENCY_SHIFTER_FREQUENCY           0x0001
-//#define AL_FREQUENCY_SHIFTER_LEFT_DIRECTION      0x0002
-//#define AL_FREQUENCY_SHIFTER_RIGHT_DIRECTION     0x0003
-//
-///* Vocal morpher effect parameters */
-//#define AL_VOCAL_MORPHER_PHONEMEA                0x0001
-//#define AL_VOCAL_MORPHER_PHONEMEA_COARSE_TUNING  0x0002
-//#define AL_VOCAL_MORPHER_PHONEMEB                0x0003
-//#define AL_VOCAL_MORPHER_PHONEMEB_COARSE_TUNING  0x0004
-//#define AL_VOCAL_MORPHER_WAVEFORM                0x0005
-//#define AL_VOCAL_MORPHER_RATE                    0x0006
-//
-///* Pitchshifter effect parameters */
-//#define AL_PITCH_SHIFTER_COARSE_TUNE             0x0001
-//#define AL_PITCH_SHIFTER_FINE_TUNE               0x0002
-//
-///* Ringmodulator effect parameters */
-//#define AL_RING_MODULATOR_FREQUENCY              0x0001
-//#define AL_RING_MODULATOR_HIGHPASS_CUTOFF        0x0002
-//#define AL_RING_MODULATOR_WAVEFORM               0x0003
-//
-///* Autowah effect parameters */
-//#define AL_AUTOWAH_ATTACK_TIME                   0x0001
-//#define AL_AUTOWAH_RELEASE_TIME                  0x0002
-//#define AL_AUTOWAH_RESONANCE                     0x0003
-//#define AL_AUTOWAH_PEAK_GAIN                     0x0004
-//
-///* Compressor effect parameters */
-//#define AL_COMPRESSOR_ONOFF                      0x0001
-//
-///* Equalizer effect parameters */
-//#define AL_EQUALIZER_LOW_GAIN                    0x0001
-//#define AL_EQUALIZER_LOW_CUTOFF                  0x0002
-//#define AL_EQUALIZER_MID1_GAIN                   0x0003
-//#define AL_EQUALIZER_MID1_CENTER                 0x0004
-//#define AL_EQUALIZER_MID1_WIDTH                  0x0005
-//#define AL_EQUALIZER_MID2_GAIN                   0x0006
-//#define AL_EQUALIZER_MID2_CENTER                 0x0007
-//#define AL_EQUALIZER_MID2_WIDTH                  0x0008
-//#define AL_EQUALIZER_HIGH_GAIN                   0x0009
-//#define AL_EQUALIZER_HIGH_CUTOFF                 0x000A
-//
-///* Effect type */
-//#define AL_EFFECT_FIRST_PARAMETER                0x0000
-//#define AL_EFFECT_LAST_PARAMETER                 0x8000
-    static final int AL_EFFECT_TYPE = 0x8001;
-//
-///* Effect types, used with the AL_EFFECT_TYPE property */
-//#define AL_EFFECT_NULL                           0x0000
-    static final int AL_EFFECT_REVERB = 0x0001;
-//#define AL_EFFECT_CHORUS                         0x0002
-//#define AL_EFFECT_DISTORTION                     0x0003
-//#define AL_EFFECT_ECHO                           0x0004
-//#define AL_EFFECT_FLANGER                        0x0005
-//#define AL_EFFECT_FREQUENCY_SHIFTER              0x0006
-//#define AL_EFFECT_VOCAL_MORPHER                  0x0007
-//#define AL_EFFECT_PITCH_SHIFTER                  0x0008
-//#define AL_EFFECT_RING_MODULATOR                 0x0009
-//#define AL_EFFECT_AUTOWAH                        0x000A
-//#define AL_EFFECT_COMPRESSOR                     0x000B
-//#define AL_EFFECT_EQUALIZER                      0x000C
-//#define AL_EFFECT_EAXREVERB                      0x8000
-//
-///* Auxiliary Effect Slot properties. */
-    static final int AL_EFFECTSLOT_EFFECT = 0x0001;
-//#define AL_EFFECTSLOT_GAIN                       0x0002
-//#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO        0x0003
-//
-///* NULL Auxiliary Slot ID to disable a source send. */
-//#define AL_EFFECTSLOT_NULL                       0x0000
-//
-//
-///* Filter properties. */
-//
-///* Lowpass filter parameters */
-    static final int AL_LOWPASS_GAIN = 0x0001;
-    static final int AL_LOWPASS_GAINHF = 0x0002;
-//
-///* Highpass filter parameters */
-//#define AL_HIGHPASS_GAIN                         0x0001
-//#define AL_HIGHPASS_GAINLF                       0x0002
-//
-///* Bandpass filter parameters */
-//#define AL_BANDPASS_GAIN                         0x0001
-//#define AL_BANDPASS_GAINLF                       0x0002
-//#define AL_BANDPASS_GAINHF                       0x0003
-//
-///* Filter type */
-//#define AL_FILTER_FIRST_PARAMETER                0x0000
-//#define AL_FILTER_LAST_PARAMETER                 0x8000
-    static final int AL_FILTER_TYPE = 0x8001;
-//
-///* Filter types, used with the AL_FILTER_TYPE property */
-    static final int AL_FILTER_NULL = 0x0000;
-    static final int AL_FILTER_LOWPASS = 0x0001;
-    static final int AL_FILTER_HIGHPASS = 0x0002;
-//#define AL_FILTER_BANDPASS                       0x0003
-//
-///* Filter ranges and defaults. */
-//
-///* Lowpass filter */
-//#define AL_LOWPASS_MIN_GAIN                      (0.0f)
-//#define AL_LOWPASS_MAX_GAIN                      (1.0f)
-//#define AL_LOWPASS_DEFAULT_GAIN                  (1.0f)
-//
-//#define AL_LOWPASS_MIN_GAINHF                    (0.0f)
-//#define AL_LOWPASS_MAX_GAINHF                    (1.0f)
-//#define AL_LOWPASS_DEFAULT_GAINHF                (1.0f)
-//
-///* Highpass filter */
-//#define AL_HIGHPASS_MIN_GAIN                     (0.0f)
-//#define AL_HIGHPASS_MAX_GAIN                     (1.0f)
-//#define AL_HIGHPASS_DEFAULT_GAIN                 (1.0f)
-//
-//#define AL_HIGHPASS_MIN_GAINLF                   (0.0f)
-//#define AL_HIGHPASS_MAX_GAINLF                   (1.0f)
-//#define AL_HIGHPASS_DEFAULT_GAINLF               (1.0f)
-//
-///* Bandpass filter */
-//#define AL_BANDPASS_MIN_GAIN                     (0.0f)
-//#define AL_BANDPASS_MAX_GAIN                     (1.0f)
-//#define AL_BANDPASS_DEFAULT_GAIN                 (1.0f)
-//
-//#define AL_BANDPASS_MIN_GAINHF                   (0.0f)
-//#define AL_BANDPASS_MAX_GAINHF                   (1.0f)
-//#define AL_BANDPASS_DEFAULT_GAINHF               (1.0f)
-//
-//#define AL_BANDPASS_MIN_GAINLF                   (0.0f)
-//#define AL_BANDPASS_MAX_GAINLF                   (1.0f)
-//#define AL_BANDPASS_DEFAULT_GAINLF               (1.0f)
-//
-//
-///* Effect parameter ranges and defaults. */
-//
-///* Standard reverb effect */
-//#define AL_REVERB_MIN_DENSITY                    (0.0f)
-//#define AL_REVERB_MAX_DENSITY                    (1.0f)
-//#define AL_REVERB_DEFAULT_DENSITY                (1.0f)
-//
-//#define AL_REVERB_MIN_DIFFUSION                  (0.0f)
-//#define AL_REVERB_MAX_DIFFUSION                  (1.0f)
-//#define AL_REVERB_DEFAULT_DIFFUSION              (1.0f)
-//
-//#define AL_REVERB_MIN_GAIN                       (0.0f)
-//#define AL_REVERB_MAX_GAIN                       (1.0f)
-//#define AL_REVERB_DEFAULT_GAIN                   (0.32f)
-//
-//#define AL_REVERB_MIN_GAINHF                     (0.0f)
-//#define AL_REVERB_MAX_GAINHF                     (1.0f)
-//#define AL_REVERB_DEFAULT_GAINHF                 (0.89f)
-//
-//#define AL_REVERB_MIN_DECAY_TIME                 (0.1f)
-//#define AL_REVERB_MAX_DECAY_TIME                 (20.0f)
-//#define AL_REVERB_DEFAULT_DECAY_TIME             (1.49f)
-//
-//#define AL_REVERB_MIN_DECAY_HFRATIO              (0.1f)
-//#define AL_REVERB_MAX_DECAY_HFRATIO              (2.0f)
-//#define AL_REVERB_DEFAULT_DECAY_HFRATIO          (0.83f)
-//
-//#define AL_REVERB_MIN_REFLECTIONS_GAIN           (0.0f)
-//#define AL_REVERB_MAX_REFLECTIONS_GAIN           (3.16f)
-//#define AL_REVERB_DEFAULT_REFLECTIONS_GAIN       (0.05f)
-//
-//#define AL_REVERB_MIN_REFLECTIONS_DELAY          (0.0f)
-//#define AL_REVERB_MAX_REFLECTIONS_DELAY          (0.3f)
-//#define AL_REVERB_DEFAULT_REFLECTIONS_DELAY      (0.007f)
-//
-//#define AL_REVERB_MIN_LATE_REVERB_GAIN           (0.0f)
-//#define AL_REVERB_MAX_LATE_REVERB_GAIN           (10.0f)
-//#define AL_REVERB_DEFAULT_LATE_REVERB_GAIN       (1.26f)
-//
-//#define AL_REVERB_MIN_LATE_REVERB_DELAY          (0.0f)
-//#define AL_REVERB_MAX_LATE_REVERB_DELAY          (0.1f)
-//#define AL_REVERB_DEFAULT_LATE_REVERB_DELAY      (0.011f)
-//
-//#define AL_REVERB_MIN_AIR_ABSORPTION_GAINHF      (0.892f)
-//#define AL_REVERB_MAX_AIR_ABSORPTION_GAINHF      (1.0f)
-//#define AL_REVERB_DEFAULT_AIR_ABSORPTION_GAINHF  (0.994f)
-//
-//#define AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR        (0.0f)
-//#define AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR        (10.0f)
-//#define AL_REVERB_DEFAULT_ROOM_ROLLOFF_FACTOR    (0.0f)
-//
-//#define AL_REVERB_MIN_DECAY_HFLIMIT              AL_FALSE
-//#define AL_REVERB_MAX_DECAY_HFLIMIT              AL_TRUE
-//#define AL_REVERB_DEFAULT_DECAY_HFLIMIT          AL_TRUE
-//
-///* EAX reverb effect */
-//#define AL_EAXREVERB_MIN_DENSITY                 (0.0f)
-//#define AL_EAXREVERB_MAX_DENSITY                 (1.0f)
-//#define AL_EAXREVERB_DEFAULT_DENSITY             (1.0f)
-//
-//#define AL_EAXREVERB_MIN_DIFFUSION               (0.0f)
-//#define AL_EAXREVERB_MAX_DIFFUSION               (1.0f)
-//#define AL_EAXREVERB_DEFAULT_DIFFUSION           (1.0f)
-//
-//#define AL_EAXREVERB_MIN_GAIN                    (0.0f)
-//#define AL_EAXREVERB_MAX_GAIN                    (1.0f)
-//#define AL_EAXREVERB_DEFAULT_GAIN                (0.32f)
-//
-//#define AL_EAXREVERB_MIN_GAINHF                  (0.0f)
-//#define AL_EAXREVERB_MAX_GAINHF                  (1.0f)
-//#define AL_EAXREVERB_DEFAULT_GAINHF              (0.89f)
-//
-//#define AL_EAXREVERB_MIN_GAINLF                  (0.0f)
-//#define AL_EAXREVERB_MAX_GAINLF                  (1.0f)
-//#define AL_EAXREVERB_DEFAULT_GAINLF              (1.0f)
-//
-//#define AL_EAXREVERB_MIN_DECAY_TIME              (0.1f)
-//#define AL_EAXREVERB_MAX_DECAY_TIME              (20.0f)
-//#define AL_EAXREVERB_DEFAULT_DECAY_TIME          (1.49f)
-//
-//#define AL_EAXREVERB_MIN_DECAY_HFRATIO           (0.1f)
-//#define AL_EAXREVERB_MAX_DECAY_HFRATIO           (2.0f)
-//#define AL_EAXREVERB_DEFAULT_DECAY_HFRATIO       (0.83f)
-//
-//#define AL_EAXREVERB_MIN_DECAY_LFRATIO           (0.1f)
-//#define AL_EAXREVERB_MAX_DECAY_LFRATIO           (2.0f)
-//#define AL_EAXREVERB_DEFAULT_DECAY_LFRATIO       (1.0f)
-//
-//#define AL_EAXREVERB_MIN_REFLECTIONS_GAIN        (0.0f)
-//#define AL_EAXREVERB_MAX_REFLECTIONS_GAIN        (3.16f)
-//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_GAIN    (0.05f)
-//
-//#define AL_EAXREVERB_MIN_REFLECTIONS_DELAY       (0.0f)
-//#define AL_EAXREVERB_MAX_REFLECTIONS_DELAY       (0.3f)
-//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_DELAY   (0.007f)
-//
-//#define AL_EAXREVERB_DEFAULT_REFLECTIONS_PAN_XYZ (0.0f)
-//
-//#define AL_EAXREVERB_MIN_LATE_REVERB_GAIN        (0.0f)
-//#define AL_EAXREVERB_MAX_LATE_REVERB_GAIN        (10.0f)
-//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_GAIN    (1.26f)
-//
-//#define AL_EAXREVERB_MIN_LATE_REVERB_DELAY       (0.0f)
-//#define AL_EAXREVERB_MAX_LATE_REVERB_DELAY       (0.1f)
-//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_DELAY   (0.011f)
-//
-//#define AL_EAXREVERB_DEFAULT_LATE_REVERB_PAN_XYZ (0.0f)
-//
-//#define AL_EAXREVERB_MIN_ECHO_TIME               (0.075f)
-//#define AL_EAXREVERB_MAX_ECHO_TIME               (0.25f)
-//#define AL_EAXREVERB_DEFAULT_ECHO_TIME           (0.25f)
-//
-//#define AL_EAXREVERB_MIN_ECHO_DEPTH              (0.0f)
-//#define AL_EAXREVERB_MAX_ECHO_DEPTH              (1.0f)
-//#define AL_EAXREVERB_DEFAULT_ECHO_DEPTH          (0.0f)
-//
-//#define AL_EAXREVERB_MIN_MODULATION_TIME         (0.04f)
-//#define AL_EAXREVERB_MAX_MODULATION_TIME         (4.0f)
-//#define AL_EAXREVERB_DEFAULT_MODULATION_TIME     (0.25f)
-//
-//#define AL_EAXREVERB_MIN_MODULATION_DEPTH        (0.0f)
-//#define AL_EAXREVERB_MAX_MODULATION_DEPTH        (1.0f)
-//#define AL_EAXREVERB_DEFAULT_MODULATION_DEPTH    (0.0f)
-//
-//#define AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF   (0.892f)
-//#define AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF   (1.0f)
-//#define AL_EAXREVERB_DEFAULT_AIR_ABSORPTION_GAINHF (0.994f)
-//
-//#define AL_EAXREVERB_MIN_HFREFERENCE             (1000.0f)
-//#define AL_EAXREVERB_MAX_HFREFERENCE             (20000.0f)
-//#define AL_EAXREVERB_DEFAULT_HFREFERENCE         (5000.0f)
-//
-//#define AL_EAXREVERB_MIN_LFREFERENCE             (20.0f)
-//#define AL_EAXREVERB_MAX_LFREFERENCE             (1000.0f)
-//#define AL_EAXREVERB_DEFAULT_LFREFERENCE         (250.0f)
-//
-//#define AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR     (0.0f)
-//#define AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR     (10.0f)
-//#define AL_EAXREVERB_DEFAULT_ROOM_ROLLOFF_FACTOR (0.0f)
-//
-//#define AL_EAXREVERB_MIN_DECAY_HFLIMIT           AL_FALSE
-//#define AL_EAXREVERB_MAX_DECAY_HFLIMIT           AL_TRUE
-//#define AL_EAXREVERB_DEFAULT_DECAY_HFLIMIT       AL_TRUE
-//
-///* Chorus effect */
-//#define AL_CHORUS_WAVEFORM_SINUSOID              (0)
-//#define AL_CHORUS_WAVEFORM_TRIANGLE              (1)
-//
-//#define AL_CHORUS_MIN_WAVEFORM                   (0)
-//#define AL_CHORUS_MAX_WAVEFORM                   (1)
-//#define AL_CHORUS_DEFAULT_WAVEFORM               (1)
-//
-//#define AL_CHORUS_MIN_PHASE                      (-180)
-//#define AL_CHORUS_MAX_PHASE                      (180)
-//#define AL_CHORUS_DEFAULT_PHASE                  (90)
-//
-//#define AL_CHORUS_MIN_RATE                       (0.0f)
-//#define AL_CHORUS_MAX_RATE                       (10.0f)
-//#define AL_CHORUS_DEFAULT_RATE                   (1.1f)
-//
-//#define AL_CHORUS_MIN_DEPTH                      (0.0f)
-//#define AL_CHORUS_MAX_DEPTH                      (1.0f)
-//#define AL_CHORUS_DEFAULT_DEPTH                  (0.1f)
-//
-//#define AL_CHORUS_MIN_FEEDBACK                   (-1.0f)
-//#define AL_CHORUS_MAX_FEEDBACK                   (1.0f)
-//#define AL_CHORUS_DEFAULT_FEEDBACK               (0.25f)
-//
-//#define AL_CHORUS_MIN_DELAY                      (0.0f)
-//#define AL_CHORUS_MAX_DELAY                      (0.016f)
-//#define AL_CHORUS_DEFAULT_DELAY                  (0.016f)
-//
-///* Distortion effect */
-//#define AL_DISTORTION_MIN_EDGE                   (0.0f)
-//#define AL_DISTORTION_MAX_EDGE                   (1.0f)
-//#define AL_DISTORTION_DEFAULT_EDGE               (0.2f)
-//
-//#define AL_DISTORTION_MIN_GAIN                   (0.01f)
-//#define AL_DISTORTION_MAX_GAIN                   (1.0f)
-//#define AL_DISTORTION_DEFAULT_GAIN               (0.05f)
-//
-//#define AL_DISTORTION_MIN_LOWPASS_CUTOFF         (80.0f)
-//#define AL_DISTORTION_MAX_LOWPASS_CUTOFF         (24000.0f)
-//#define AL_DISTORTION_DEFAULT_LOWPASS_CUTOFF     (8000.0f)
-//
-//#define AL_DISTORTION_MIN_EQCENTER               (80.0f)
-//#define AL_DISTORTION_MAX_EQCENTER               (24000.0f)
-//#define AL_DISTORTION_DEFAULT_EQCENTER           (3600.0f)
-//
-//#define AL_DISTORTION_MIN_EQBANDWIDTH            (80.0f)
-//#define AL_DISTORTION_MAX_EQBANDWIDTH            (24000.0f)
-//#define AL_DISTORTION_DEFAULT_EQBANDWIDTH        (3600.0f)
-//
-///* Echo effect */
-//#define AL_ECHO_MIN_DELAY                        (0.0f)
-//#define AL_ECHO_MAX_DELAY                        (0.207f)
-//#define AL_ECHO_DEFAULT_DELAY                    (0.1f)
-//
-//#define AL_ECHO_MIN_LRDELAY                      (0.0f)
-//#define AL_ECHO_MAX_LRDELAY                      (0.404f)
-//#define AL_ECHO_DEFAULT_LRDELAY                  (0.1f)
-//
-//#define AL_ECHO_MIN_DAMPING                      (0.0f)
-//#define AL_ECHO_MAX_DAMPING                      (0.99f)
-//#define AL_ECHO_DEFAULT_DAMPING                  (0.5f)
-//
-//#define AL_ECHO_MIN_FEEDBACK                     (0.0f)
-//#define AL_ECHO_MAX_FEEDBACK                     (1.0f)
-//#define AL_ECHO_DEFAULT_FEEDBACK                 (0.5f)
-//
-//#define AL_ECHO_MIN_SPREAD                       (-1.0f)
-//#define AL_ECHO_MAX_SPREAD                       (1.0f)
-//#define AL_ECHO_DEFAULT_SPREAD                   (-1.0f)
-//
-///* Flanger effect */
-//#define AL_FLANGER_WAVEFORM_SINUSOID             (0)
-//#define AL_FLANGER_WAVEFORM_TRIANGLE             (1)
-//
-//#define AL_FLANGER_MIN_WAVEFORM                  (0)
-//#define AL_FLANGER_MAX_WAVEFORM                  (1)
-//#define AL_FLANGER_DEFAULT_WAVEFORM              (1)
-//
-//#define AL_FLANGER_MIN_PHASE                     (-180)
-//#define AL_FLANGER_MAX_PHASE                     (180)
-//#define AL_FLANGER_DEFAULT_PHASE                 (0)
-//
-//#define AL_FLANGER_MIN_RATE                      (0.0f)
-//#define AL_FLANGER_MAX_RATE                      (10.0f)
-//#define AL_FLANGER_DEFAULT_RATE                  (0.27f)
-//
-//#define AL_FLANGER_MIN_DEPTH                     (0.0f)
-//#define AL_FLANGER_MAX_DEPTH                     (1.0f)
-//#define AL_FLANGER_DEFAULT_DEPTH                 (1.0f)
-//
-//#define AL_FLANGER_MIN_FEEDBACK                  (-1.0f)
-//#define AL_FLANGER_MAX_FEEDBACK                  (1.0f)
-//#define AL_FLANGER_DEFAULT_FEEDBACK              (-0.5f)
-//
-//#define AL_FLANGER_MIN_DELAY                     (0.0f)
-//#define AL_FLANGER_MAX_DELAY                     (0.004f)
-//#define AL_FLANGER_DEFAULT_DELAY                 (0.002f)
-//
-///* Frequency shifter effect */
-//#define AL_FREQUENCY_SHIFTER_MIN_FREQUENCY       (0.0f)
-//#define AL_FREQUENCY_SHIFTER_MAX_FREQUENCY       (24000.0f)
-//#define AL_FREQUENCY_SHIFTER_DEFAULT_FREQUENCY   (0.0f)
-//
-//#define AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION  (0)
-//#define AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION  (2)
-//#define AL_FREQUENCY_SHIFTER_DEFAULT_LEFT_DIRECTION (0)
-//
-//#define AL_FREQUENCY_SHIFTER_DIRECTION_DOWN      (0)
-//#define AL_FREQUENCY_SHIFTER_DIRECTION_UP        (1)
-//#define AL_FREQUENCY_SHIFTER_DIRECTION_OFF       (2)
-//
-//#define AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION (0)
-//#define AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION (2)
-//#define AL_FREQUENCY_SHIFTER_DEFAULT_RIGHT_DIRECTION (0)
-//
-///* Vocal morpher effect */
-//#define AL_VOCAL_MORPHER_MIN_PHONEMEA            (0)
-//#define AL_VOCAL_MORPHER_MAX_PHONEMEA            (29)
-//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA        (0)
-//
-//#define AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING (-24)
-//#define AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING (24)
-//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEA_COARSE_TUNING (0)
-//
-//#define AL_VOCAL_MORPHER_MIN_PHONEMEB            (0)
-//#define AL_VOCAL_MORPHER_MAX_PHONEMEB            (29)
-//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB        (10)
-//
-//#define AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING (-24)
-//#define AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING (24)
-//#define AL_VOCAL_MORPHER_DEFAULT_PHONEMEB_COARSE_TUNING (0)
-//
-//#define AL_VOCAL_MORPHER_PHONEME_A               (0)
-//#define AL_VOCAL_MORPHER_PHONEME_E               (1)
-//#define AL_VOCAL_MORPHER_PHONEME_I               (2)
-//#define AL_VOCAL_MORPHER_PHONEME_O               (3)
-//#define AL_VOCAL_MORPHER_PHONEME_U               (4)
-//#define AL_VOCAL_MORPHER_PHONEME_AA              (5)
-//#define AL_VOCAL_MORPHER_PHONEME_AE              (6)
-//#define AL_VOCAL_MORPHER_PHONEME_AH              (7)
-//#define AL_VOCAL_MORPHER_PHONEME_AO              (8)
-//#define AL_VOCAL_MORPHER_PHONEME_EH              (9)
-//#define AL_VOCAL_MORPHER_PHONEME_ER              (10)
-//#define AL_VOCAL_MORPHER_PHONEME_IH              (11)
-//#define AL_VOCAL_MORPHER_PHONEME_IY              (12)
-//#define AL_VOCAL_MORPHER_PHONEME_UH              (13)
-//#define AL_VOCAL_MORPHER_PHONEME_UW              (14)
-//#define AL_VOCAL_MORPHER_PHONEME_B               (15)
-//#define AL_VOCAL_MORPHER_PHONEME_D               (16)
-//#define AL_VOCAL_MORPHER_PHONEME_F               (17)
-//#define AL_VOCAL_MORPHER_PHONEME_G               (18)
-//#define AL_VOCAL_MORPHER_PHONEME_J               (19)
-//#define AL_VOCAL_MORPHER_PHONEME_K               (20)
-//#define AL_VOCAL_MORPHER_PHONEME_L               (21)
-//#define AL_VOCAL_MORPHER_PHONEME_M               (22)
-//#define AL_VOCAL_MORPHER_PHONEME_N               (23)
-//#define AL_VOCAL_MORPHER_PHONEME_P               (24)
-//#define AL_VOCAL_MORPHER_PHONEME_R               (25)
-//#define AL_VOCAL_MORPHER_PHONEME_S               (26)
-//#define AL_VOCAL_MORPHER_PHONEME_T               (27)
-//#define AL_VOCAL_MORPHER_PHONEME_V               (28)
-//#define AL_VOCAL_MORPHER_PHONEME_Z               (29)
-//
-//#define AL_VOCAL_MORPHER_WAVEFORM_SINUSOID       (0)
-//#define AL_VOCAL_MORPHER_WAVEFORM_TRIANGLE       (1)
-//#define AL_VOCAL_MORPHER_WAVEFORM_SAWTOOTH       (2)
-//
-//#define AL_VOCAL_MORPHER_MIN_WAVEFORM            (0)
-//#define AL_VOCAL_MORPHER_MAX_WAVEFORM            (2)
-//#define AL_VOCAL_MORPHER_DEFAULT_WAVEFORM        (0)
-//
-//#define AL_VOCAL_MORPHER_MIN_RATE                (0.0f)
-//#define AL_VOCAL_MORPHER_MAX_RATE                (10.0f)
-//#define AL_VOCAL_MORPHER_DEFAULT_RATE            (1.41f)
-//
-///* Pitch shifter effect */
-//#define AL_PITCH_SHIFTER_MIN_COARSE_TUNE         (-12)
-//#define AL_PITCH_SHIFTER_MAX_COARSE_TUNE         (12)
-//#define AL_PITCH_SHIFTER_DEFAULT_COARSE_TUNE     (12)
-//
-//#define AL_PITCH_SHIFTER_MIN_FINE_TUNE           (-50)
-//#define AL_PITCH_SHIFTER_MAX_FINE_TUNE           (50)
-//#define AL_PITCH_SHIFTER_DEFAULT_FINE_TUNE       (0)
-//
-///* Ring modulator effect */
-//#define AL_RING_MODULATOR_MIN_FREQUENCY          (0.0f)
-//#define AL_RING_MODULATOR_MAX_FREQUENCY          (8000.0f)
-//#define AL_RING_MODULATOR_DEFAULT_FREQUENCY      (440.0f)
-//
-//#define AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF    (0.0f)
-//#define AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF    (24000.0f)
-//#define AL_RING_MODULATOR_DEFAULT_HIGHPASS_CUTOFF (800.0f)
-//
-//#define AL_RING_MODULATOR_SINUSOID               (0)
-//#define AL_RING_MODULATOR_SAWTOOTH               (1)
-//#define AL_RING_MODULATOR_SQUARE                 (2)
-//
-//#define AL_RING_MODULATOR_MIN_WAVEFORM           (0)
-//#define AL_RING_MODULATOR_MAX_WAVEFORM           (2)
-//#define AL_RING_MODULATOR_DEFAULT_WAVEFORM       (0)
-//
-///* Autowah effect */
-//#define AL_AUTOWAH_MIN_ATTACK_TIME               (0.0001f)
-//#define AL_AUTOWAH_MAX_ATTACK_TIME               (1.0f)
-//#define AL_AUTOWAH_DEFAULT_ATTACK_TIME           (0.06f)
-//
-//#define AL_AUTOWAH_MIN_RELEASE_TIME              (0.0001f)
-//#define AL_AUTOWAH_MAX_RELEASE_TIME              (1.0f)
-//#define AL_AUTOWAH_DEFAULT_RELEASE_TIME          (0.06f)
-//
-//#define AL_AUTOWAH_MIN_RESONANCE                 (2.0f)
-//#define AL_AUTOWAH_MAX_RESONANCE                 (1000.0f)
-//#define AL_AUTOWAH_DEFAULT_RESONANCE             (1000.0f)
-//
-//#define AL_AUTOWAH_MIN_PEAK_GAIN                 (0.00003f)
-//#define AL_AUTOWAH_MAX_PEAK_GAIN                 (31621.0f)
-//#define AL_AUTOWAH_DEFAULT_PEAK_GAIN             (11.22f)
-//
-///* Compressor effect */
-//#define AL_COMPRESSOR_MIN_ONOFF                  (0)
-//#define AL_COMPRESSOR_MAX_ONOFF                  (1)
-//#define AL_COMPRESSOR_DEFAULT_ONOFF              (1)
-//
-///* Equalizer effect */
-//#define AL_EQUALIZER_MIN_LOW_GAIN                (0.126f)
-//#define AL_EQUALIZER_MAX_LOW_GAIN                (7.943f)
-//#define AL_EQUALIZER_DEFAULT_LOW_GAIN            (1.0f)
-//
-//#define AL_EQUALIZER_MIN_LOW_CUTOFF              (50.0f)
-//#define AL_EQUALIZER_MAX_LOW_CUTOFF              (800.0f)
-//#define AL_EQUALIZER_DEFAULT_LOW_CUTOFF          (200.0f)
-//
-//#define AL_EQUALIZER_MIN_MID1_GAIN               (0.126f)
-//#define AL_EQUALIZER_MAX_MID1_GAIN               (7.943f)
-//#define AL_EQUALIZER_DEFAULT_MID1_GAIN           (1.0f)
-//
-//#define AL_EQUALIZER_MIN_MID1_CENTER             (200.0f)
-//#define AL_EQUALIZER_MAX_MID1_CENTER             (3000.0f)
-//#define AL_EQUALIZER_DEFAULT_MID1_CENTER         (500.0f)
-//
-//#define AL_EQUALIZER_MIN_MID1_WIDTH              (0.01f)
-//#define AL_EQUALIZER_MAX_MID1_WIDTH              (1.0f)
-//#define AL_EQUALIZER_DEFAULT_MID1_WIDTH          (1.0f)
-//
-//#define AL_EQUALIZER_MIN_MID2_GAIN               (0.126f)
-//#define AL_EQUALIZER_MAX_MID2_GAIN               (7.943f)
-//#define AL_EQUALIZER_DEFAULT_MID2_GAIN           (1.0f)
-//
-//#define AL_EQUALIZER_MIN_MID2_CENTER             (1000.0f)
-//#define AL_EQUALIZER_MAX_MID2_CENTER             (8000.0f)
-//#define AL_EQUALIZER_DEFAULT_MID2_CENTER         (3000.0f)
-//
-//#define AL_EQUALIZER_MIN_MID2_WIDTH              (0.01f)
-//#define AL_EQUALIZER_MAX_MID2_WIDTH              (1.0f)
-//#define AL_EQUALIZER_DEFAULT_MID2_WIDTH          (1.0f)
-//
-//#define AL_EQUALIZER_MIN_HIGH_GAIN               (0.126f)
-//#define AL_EQUALIZER_MAX_HIGH_GAIN               (7.943f)
-//#define AL_EQUALIZER_DEFAULT_HIGH_GAIN           (1.0f)
-//
-//#define AL_EQUALIZER_MIN_HIGH_CUTOFF             (4000.0f)
-//#define AL_EQUALIZER_MAX_HIGH_CUTOFF             (16000.0f)
-//#define AL_EQUALIZER_DEFAULT_HIGH_CUTOFF         (6000.0f)
-//
-//
-///* Source parameter value ranges and defaults. */
-//#define AL_MIN_AIR_ABSORPTION_FACTOR             (0.0f)
-//#define AL_MAX_AIR_ABSORPTION_FACTOR             (10.0f)
-//#define AL_DEFAULT_AIR_ABSORPTION_FACTOR         (0.0f)
-//
-//#define AL_MIN_ROOM_ROLLOFF_FACTOR               (0.0f)
-//#define AL_MAX_ROOM_ROLLOFF_FACTOR               (10.0f)
-//#define AL_DEFAULT_ROOM_ROLLOFF_FACTOR           (0.0f)
-//
-//#define AL_MIN_CONE_OUTER_GAINHF                 (0.0f)
-//#define AL_MAX_CONE_OUTER_GAINHF                 (1.0f)
-//#define AL_DEFAULT_CONE_OUTER_GAINHF             (1.0f)
-//
-//#define AL_MIN_DIRECT_FILTER_GAINHF_AUTO         AL_FALSE
-//#define AL_MAX_DIRECT_FILTER_GAINHF_AUTO         AL_TRUE
-//#define AL_DEFAULT_DIRECT_FILTER_GAINHF_AUTO     AL_TRUE
-//
-//#define AL_MIN_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_FALSE
-//#define AL_MAX_AUXILIARY_SEND_FILTER_GAIN_AUTO   AL_TRUE
-//#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAIN_AUTO AL_TRUE
-//
-//#define AL_MIN_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_FALSE
-//#define AL_MAX_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
-//#define AL_DEFAULT_AUXILIARY_SEND_FILTER_GAINHF_AUTO AL_TRUE
-//
-//
-///* Listener parameter value ranges and defaults. */
-//#define AL_MIN_METERS_PER_UNIT                   FLT_MIN
-//#define AL_MAX_METERS_PER_UNIT                   FLT_MAX
-//#define AL_DEFAULT_METERS_PER_UNIT               (1.0f)
-
-
-    public static String GetALErrorMsg(int errorCode) {
-        String errorText;
-        switch (errorCode) {
-            case AL_NO_ERROR:
-                errorText = "No Error";
-                break;
-            case AL_INVALID_NAME:
-                errorText = "Invalid Name";
-                break;
-            case AL_INVALID_ENUM:
-                errorText = "Invalid Enum";
-                break;
-            case AL_INVALID_VALUE:
-                errorText = "Invalid Value";
-                break;
-            case AL_INVALID_OPERATION:
-                errorText = "Invalid Operation";
-                break;
-            case AL_OUT_OF_MEMORY:
-                errorText = "Out of Memory";
-                break;
-            default:
-                errorText = "Unknown Error Code: " + String.valueOf(errorCode);
-        }
-        return errorText;
-    }
-}
-

+ 0 - 67
jme3-ios/src/main/java/com/jme3/audio/android/AndroidAudioData.java

@@ -1,67 +0,0 @@
-package com.jme3.audio.android;
-
-import com.jme3.asset.AssetKey;
-import com.jme3.audio.AudioData;
-import com.jme3.audio.AudioRenderer;
-import com.jme3.util.NativeObject;
-
-public class AndroidAudioData extends AudioData {
-
-    protected AssetKey<?> assetKey;
-    protected float currentVolume = 0f;
-
-    public AndroidAudioData(){
-        super();
-    }
-    
-    protected AndroidAudioData(int id){
-        super(id);
-    }
-    
-    public AssetKey<?> getAssetKey() {
-        return assetKey;
-    }
-
-    public void setAssetKey(AssetKey<?> assetKey) {
-        this.assetKey = assetKey;
-    }
-
-    @Override
-    public DataType getDataType() {
-        return DataType.Buffer;
-    }
-
-    @Override
-    public float getDuration() {
-        return 0; // TODO: ???
-    }
-
-    @Override
-    public void resetObject() {
-        this.id = -1;
-        setUpdateNeeded();  
-    }
-
-    @Override
-    public void deleteObject(Object rendererObject) {
-        ((AudioRenderer)rendererObject).deleteAudioData(this);
-    }
-
-    public float getCurrentVolume() {
-        return currentVolume;
-    }
-
-    public void setCurrentVolume(float currentVolume) {
-        this.currentVolume = currentVolume;
-    }
-
-    @Override
-    public NativeObject createDestructableClone() {
-        return new AndroidAudioData(id);
-    }
-
-    @Override
-    public long getUniqueId() {
-        return ((long)OBJTYPE_AUDIOBUFFER << 32) | ((long)id);
-    }
-}

+ 53 - 0
jme3-ios/src/main/java/com/jme3/audio/ios/IosAL.java

@@ -0,0 +1,53 @@
+package com.jme3.audio.ios;
+
+import com.jme3.audio.openal.AL;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+public final class IosAL implements AL {
+
+    public IosAL() {
+    }
+
+    public native String alGetString(int parameter);
+
+    public native int alGenSources();
+
+    public native int alGetError();
+
+    public native void alDeleteSources(int numSources, IntBuffer sources);
+
+    public native void alGenBuffers(int numBuffers, IntBuffer buffers);
+
+    public native void alDeleteBuffers(int numBuffers, IntBuffer buffers);
+
+    public native void alSourceStop(int source);
+
+    public native void alSourcei(int source, int param, int value);
+
+    public native void alBufferData(int buffer, int format, ByteBuffer data, int size, int frequency);
+
+    public native void alSourcePlay(int source);
+
+    public native void alSourcePause(int source);
+
+    public native void alSourcef(int source, int param, float value);
+
+    public native void alSource3f(int source, int param, float value1, float value2, float value3);
+
+    public native int alGetSourcei(int source, int param);
+
+    public native void alSourceUnqueueBuffers(int source, int numBuffers, IntBuffer buffers);
+
+    public native void alSourceQueueBuffers(int source, int numBuffers, IntBuffer buffers);
+
+    public native void alListener(int param, FloatBuffer data);
+
+    public native void alListenerf(int param, float value);
+
+    public native void alListener3f(int param, float value1, float value2, float value3);
+
+    public native void alSource3i(int source, int param, int value1, int value2, int value3);
+
+}

+ 26 - 0
jme3-ios/src/main/java/com/jme3/audio/ios/IosALC.java

@@ -0,0 +1,26 @@
+package com.jme3.audio.ios;
+
+import com.jme3.audio.openal.ALC;
+import java.nio.IntBuffer;
+
+public final class IosALC implements ALC {
+    
+    public IosALC() {
+    }
+
+    public native void createALC();
+
+    public native void destroyALC();
+
+    public native boolean isCreated();
+
+    public native String alcGetString(int parameter);
+    
+    public native boolean alcIsExtensionPresent(String extension);
+    
+    public native void alcGetInteger(int param, IntBuffer buffer, int size);
+    
+    public native void alcDevicePauseSOFT();
+    
+    public native void alcDeviceResumeSOFT();
+}

+ 32 - 0
jme3-ios/src/main/java/com/jme3/audio/ios/IosEFX.java

@@ -0,0 +1,32 @@
+package com.jme3.audio.ios;
+
+import com.jme3.audio.openal.EFX;
+import java.nio.IntBuffer;
+
+public class IosEFX implements EFX {
+
+    public IosEFX() {
+    }
+
+    public native void alGenAuxiliaryEffectSlots(int numSlots, IntBuffer buffers);
+
+    public native void alGenEffects(int numEffects, IntBuffer buffers);
+
+    public native void alEffecti(int effect, int param, int value);
+
+    public native void alAuxiliaryEffectSloti(int effectSlot, int param, int value);
+
+    public native void alDeleteEffects(int numEffects, IntBuffer buffers);
+
+    public native void alDeleteAuxiliaryEffectSlots(int numEffectSlots, IntBuffer buffers);
+
+    public native void alGenFilters(int numFilters, IntBuffer buffers);
+
+    public native void alFilteri(int filter, int param, int value);
+
+    public native void alFilterf(int filter, int param, float value);
+
+    public native void alDeleteFilters(int numFilters, IntBuffer buffers);
+
+    public native void alEffectf(int effect, int param, float value);
+}

+ 0 - 20
jme3-ios/src/main/java/com/jme3/audio/plugins/AndroidAudioLoader.java

@@ -1,20 +0,0 @@
-package com.jme3.audio.plugins;
-
-import com.jme3.asset.AssetInfo;
-import com.jme3.asset.AssetLoader;
-import com.jme3.audio.android.AndroidAudioData;
-import java.io.IOException;
-
-/**
- * <code>AndroidAudioLoader</code> will create an 
- * {@link AndroidAudioData} object with the specified asset key.
- */
-public class AndroidAudioLoader implements AssetLoader {
-
-    @Override
-    public Object load(AssetInfo assetInfo) throws IOException {
-        AndroidAudioData result = new AndroidAudioData();
-        result.setAssetKey(assetInfo.getKey());
-        return result;
-    }
-}

+ 7 - 1
jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java

@@ -34,6 +34,7 @@ package com.jme3.renderer.ios;
 import com.jme3.renderer.RendererException;
 import com.jme3.renderer.opengl.GL;
 import com.jme3.renderer.opengl.GLExt;
+import com.jme3.renderer.opengl.GLFbo;
 import java.nio.Buffer;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
@@ -46,10 +47,13 @@ import java.nio.ShortBuffer;
  * 
  * @author Kirill Vainer
  */
-public class IosGL implements GL, GLExt {
+public class IosGL implements GL, GLExt, GLFbo {
     
     private final int[] temp_array = new int[16];
     
+    public void resetStats() {
+    }
+    
     private static int getLimitBytes(ByteBuffer buffer) {
         checkLimit(buffer);
         return buffer.limit();
@@ -90,7 +94,9 @@ public class IosGL implements GL, GLExt {
         if (buffer.remaining() < n) { 
             throw new BufferOverflowException();
         }
+        int pos = buffer.position();
         buffer.put(array, 0, n);
+        buffer.position(pos);
     }
     
     private static void checkLimit(Buffer buffer) {

+ 3 - 2
jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java

@@ -40,6 +40,7 @@ import com.jme3.renderer.ios.IosGL;
 import com.jme3.renderer.opengl.GL;
 import com.jme3.renderer.opengl.GLDebugES;
 import com.jme3.renderer.opengl.GLExt;
+import com.jme3.renderer.opengl.GLFbo;
 import com.jme3.renderer.opengl.GLRenderer;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
@@ -158,11 +159,11 @@ public class IGLESContext implements JmeContext {
         GLExt glext = (GLExt) gl;
 
 //        if (settings.getBoolean("GraphicsDebug")) {
-            gl = new GLDebugES(gl, glext);
+            gl = new GLDebugES(gl, glext, (GLFbo) glext);
             glext = (GLExt) gl;
 //        }
 
-        renderer = new GLRenderer(gl, glext);
+        renderer = new GLRenderer(gl, glext, (GLFbo) glext);
         renderer.initialize();
         
         input = new IosInputHandler();

+ 9 - 6
jme3-ios/src/main/java/com/jme3/system/ios/IosImageLoader.java

@@ -33,6 +33,7 @@ package com.jme3.system.ios;
 
 import com.jme3.asset.AssetInfo;
 import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
 import com.jme3.texture.Image;
 import com.jme3.texture.Image.Format;
 import java.io.IOException;
@@ -45,14 +46,16 @@ import java.io.InputStream;
 public class IosImageLoader implements AssetLoader {
 
     public Object load(AssetInfo info) throws IOException {
-        InputStream in = info.openStream();
+        boolean flip = ((TextureKey) info.getKey()).isFlipY();
         Image img = null;
+        InputStream in = null;
         try {
-            img = loadImageData(Image.Format.RGBA8, in);
-        } catch (Exception e) {
-            e.printStackTrace();
+        	in = info.openStream();
+            img = loadImageData(Format.RGBA8, flip, in);
         } finally {
-            in.close();
+        	if (in != null) {
+            	in.close();
+            }
         }
         return img;
     }
@@ -64,5 +67,5 @@ public class IosImageLoader implements AssetLoader {
      * @param inputStream the InputStream to load the image data from
      * @return the loaded Image
      */
-    private static native Image loadImageData(Format format, InputStream inputStream);
+    private static native Image loadImageData(Format format, boolean flipY, InputStream inputStream);
 }

+ 13 - 2
jme3-ios/src/main/java/com/jme3/system/ios/JmeIosSystem.java

@@ -36,6 +36,14 @@ import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext;
 import com.jme3.system.JmeSystemDelegate;
 import com.jme3.system.NullContext;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.audio.ios.IosAL;
+import com.jme3.audio.ios.IosALC;
+//import com.jme3.audio.ios.IosEFX;
+import com.jme3.audio.openal.AL;
+import com.jme3.audio.openal.ALAudioRenderer;
+import com.jme3.audio.openal.ALC;
+import com.jme3.audio.openal.EFX;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URL;
@@ -89,8 +97,11 @@ public class JmeIosSystem extends JmeSystemDelegate {
 
     @Override
     public AudioRenderer newAudioRenderer(AppSettings settings) {
-        return null;
-    }
+        ALC alc = new IosALC();
+        AL al = new IosAL();
+        //EFX efx = new IosEFX();
+        return new ALAudioRenderer(al, alc, null);
+     }
 
     @Override
     public void initialize(AppSettings settings) {

+ 8 - 0
jme3-ios/src/main/resources/com/jme3/asset/IOS.cfg

@@ -0,0 +1,8 @@
+INCLUDE com/jme3/asset/General.cfg
+
+# IOS specific loaders
+LOADER com.jme3.system.ios.IosImageLoader : jpg, bmp, gif, png, jpeg
+LOADER com.jme3.material.plugins.J3MLoader : j3m, j3md
+LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib
+LOADER com.jme3.export.binary.BinaryImporter : j3o
+LOADER com.jme3.font.plugins.BitmapFontLoader : fnt

+ 14 - 1
jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java

@@ -13,7 +13,7 @@ import java.nio.ShortBuffer;
 import com.jme3.renderer.opengl.GL4;
 import org.lwjgl.opengl.*;
 
-public class LwjglGL implements GL, GL2, GL3,GL4 {
+public class LwjglGL implements GL, GL2, GL3, GL4 {
     
     private static void checkLimit(Buffer buffer) {
         if (buffer == null) {
@@ -27,6 +27,9 @@ public class LwjglGL implements GL, GL2, GL3,GL4 {
         }
     }
     
+    public void resetStats() {
+    }
+    
     public void glActiveTexture(int param1) {
         GL13.glActiveTexture(param1);
     }
@@ -237,6 +240,10 @@ public class LwjglGL implements GL, GL2, GL3,GL4 {
     public String glGetString(int param1) {
         return GL11.glGetString(param1);
     }
+    
+    public String glGetString(int param1, int param2) {
+        return GL30.glGetStringi(param1, param2);
+    }
 
     public boolean glIsEnabled(int param1) {
         return GL11.glIsEnabled(param1);
@@ -444,4 +451,10 @@ public class LwjglGL implements GL, GL2, GL3,GL4 {
     public void glPatchParameter(int count) {
         GL40.glPatchParameteri(GL40.GL_PATCH_VERTICES,count);
     }
+    
+    @Override
+    public void glDeleteVertexArrays(IntBuffer arrays) {
+        checkLimit(arrays);
+        ARBVertexArrayObject.glDeleteVertexArrays(arrays);
+    }
 }

+ 8 - 60
jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java

@@ -7,12 +7,8 @@ import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
 import org.lwjgl.opengl.ARBDrawInstanced;
 import org.lwjgl.opengl.ARBInstancedArrays;
-import org.lwjgl.opengl.ARBPixelBufferObject;
 import org.lwjgl.opengl.ARBSync;
 import org.lwjgl.opengl.ARBTextureMultisample;
-import org.lwjgl.opengl.EXTFramebufferBlit;
-import org.lwjgl.opengl.EXTFramebufferMultisample;
-import org.lwjgl.opengl.EXTFramebufferObject;
 import org.lwjgl.opengl.GL15;
 import org.lwjgl.opengl.GL20;
 import org.lwjgl.opengl.GLSync;
@@ -30,99 +26,51 @@ public class LwjglGLExt implements GLExt {
             throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error");
         }
     }
-    
-    public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) {
-        EXTFramebufferBlit.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
-    }
 
+    @Override
     public void glBufferData(int target, IntBuffer data, int usage) {
         checkLimit(data);
         GL15.glBufferData(target, data, usage);
     }
 
+    @Override
     public void glBufferSubData(int target, long offset, IntBuffer data) {
         checkLimit(data);
         GL15.glBufferSubData(target, offset, data);
     }
 
+    @Override
     public void glDrawArraysInstancedARB(int mode, int first, int count, int primcount) {
         ARBDrawInstanced.glDrawArraysInstancedARB(mode, first, count, primcount);
     }
 
+    @Override
     public void glDrawBuffers(IntBuffer bufs) {
         checkLimit(bufs);
         GL20.glDrawBuffers(bufs);
     }
 
+    @Override
     public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount) {
         ARBDrawInstanced.glDrawElementsInstancedARB(mode, indices_count, type, indices_buffer_offset, primcount);
     }
 
+    @Override
     public void glGetMultisample(int pname, int index, FloatBuffer val) {
         checkLimit(val);
         ARBTextureMultisample.glGetMultisample(pname, index, val);
     }
 
-    public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) {
-        EXTFramebufferMultisample.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height);
-    }
-
+    @Override
     public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations) {
         ARBTextureMultisample.glTexImage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations);
     }
 
+    @Override
     public void glVertexAttribDivisorARB(int index, int divisor) {
         ARBInstancedArrays.glVertexAttribDivisorARB(index, divisor);
     }
 
-    public void glBindFramebufferEXT(int param1, int param2) {
-        EXTFramebufferObject.glBindFramebufferEXT(param1, param2);
-    }
-
-    public void glBindRenderbufferEXT(int param1, int param2) {
-        EXTFramebufferObject.glBindRenderbufferEXT(param1, param2);
-    }
-
-    public int glCheckFramebufferStatusEXT(int param1) {
-        return EXTFramebufferObject.glCheckFramebufferStatusEXT(param1);
-    }
-
-    public void glDeleteFramebuffersEXT(IntBuffer param1) {
-        checkLimit(param1);
-        EXTFramebufferObject.glDeleteFramebuffersEXT(param1);
-    }
-
-    public void glDeleteRenderbuffersEXT(IntBuffer param1) {
-        checkLimit(param1);
-        EXTFramebufferObject.glDeleteRenderbuffersEXT(param1);
-    }
-
-    public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) {
-        EXTFramebufferObject.glFramebufferRenderbufferEXT(param1, param2, param3, param4);
-    }
-
-    public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) {
-        EXTFramebufferObject.glFramebufferTexture2DEXT(param1, param2, param3, param4, param5);
-    }
-
-    public void glGenFramebuffersEXT(IntBuffer param1) {
-        checkLimit(param1);
-        EXTFramebufferObject.glGenFramebuffersEXT(param1);
-    }
-
-    public void glGenRenderbuffersEXT(IntBuffer param1) {
-        checkLimit(param1);
-        EXTFramebufferObject.glGenRenderbuffersEXT(param1);
-    }
-
-    public void glGenerateMipmapEXT(int param1) {
-        EXTFramebufferObject.glGenerateMipmapEXT(param1);
-    }
-
-    public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) {
-        EXTFramebufferObject.glRenderbufferStorageEXT(param1, param2, param3, param4);
-    }
-    
     @Override
     public Object glFenceSync(int condition, int flags) {
         return ARBSync.glFenceSync(condition, flags);

+ 98 - 0
jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboEXT.java

@@ -0,0 +1,98 @@
+package com.jme3.renderer.lwjgl;
+
+import com.jme3.renderer.RendererException;
+import com.jme3.renderer.opengl.GLFbo;
+import java.nio.Buffer;
+import java.nio.IntBuffer;
+import org.lwjgl.opengl.EXTFramebufferBlit;
+import org.lwjgl.opengl.EXTFramebufferMultisample;
+import org.lwjgl.opengl.EXTFramebufferObject;
+
+/**
+ * Implements GLFbo via GL_EXT_framebuffer_object.
+ * 
+ * @author Kirill Vainer
+ */
+public class LwjglGLFboEXT implements GLFbo {
+
+    private static void checkLimit(Buffer buffer) {
+        if (buffer == null) {
+            return;
+        }
+        if (buffer.limit() == 0) {
+            throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error");
+        }
+        if (buffer.remaining() == 0) {
+            throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error");
+        }
+    }
+    
+    @Override
+    public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) {
+        EXTFramebufferBlit.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+    }
+    
+    @Override
+    public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) {
+        EXTFramebufferMultisample.glRenderbufferStorageMultisampleEXT(target, samples, internalformat, width, height);
+    }
+    
+    @Override
+    public void glBindFramebufferEXT(int param1, int param2) {
+        EXTFramebufferObject.glBindFramebufferEXT(param1, param2);
+    }
+    
+    @Override
+    public void glBindRenderbufferEXT(int param1, int param2) {
+        EXTFramebufferObject.glBindRenderbufferEXT(param1, param2);
+    }
+    
+    @Override
+    public int glCheckFramebufferStatusEXT(int param1) {
+        return EXTFramebufferObject.glCheckFramebufferStatusEXT(param1);
+    }
+    
+    @Override
+    public void glDeleteFramebuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        EXTFramebufferObject.glDeleteFramebuffersEXT(param1);
+    }
+    
+    @Override
+    public void glDeleteRenderbuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        EXTFramebufferObject.glDeleteRenderbuffersEXT(param1);
+    }
+    
+    @Override
+    public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) {
+        EXTFramebufferObject.glFramebufferRenderbufferEXT(param1, param2, param3, param4);
+    }
+    
+    @Override
+    public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) {
+        EXTFramebufferObject.glFramebufferTexture2DEXT(param1, param2, param3, param4, param5);
+    }
+    
+    @Override
+    public void glGenFramebuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        EXTFramebufferObject.glGenFramebuffersEXT(param1);
+    }
+    
+    @Override
+    public void glGenRenderbuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        EXTFramebufferObject.glGenRenderbuffersEXT(param1);
+    }
+    
+    @Override
+    public void glGenerateMipmapEXT(int param1) {
+        EXTFramebufferObject.glGenerateMipmapEXT(param1);
+    }
+    
+    @Override
+    public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) {
+        EXTFramebufferObject.glRenderbufferStorageEXT(param1, param2, param3, param4);
+    }
+}

+ 96 - 0
jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLFboGL3.java

@@ -0,0 +1,96 @@
+package com.jme3.renderer.lwjgl;
+
+import com.jme3.renderer.RendererException;
+import com.jme3.renderer.opengl.GLFbo;
+import java.nio.Buffer;
+import java.nio.IntBuffer;
+import org.lwjgl.opengl.GL30;
+
+/**
+ * Implements GLFbo via OpenGL3+.
+ * 
+ * @author Kirill Vainer
+ */
+public class LwjglGLFboGL3 implements GLFbo {
+
+    private static void checkLimit(Buffer buffer) {
+        if (buffer == null) {
+            return;
+        }
+        if (buffer.limit() == 0) {
+            throw new RendererException("Attempting to upload empty buffer (limit = 0), that's an error");
+        }
+        if (buffer.remaining() == 0) {
+            throw new RendererException("Attempting to upload empty buffer (remaining = 0), that's an error");
+        }
+    }
+    
+    @Override
+    public void glBlitFramebufferEXT(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) {
+        GL30.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+    }
+    
+    @Override
+    public void glRenderbufferStorageMultisampleEXT(int target, int samples, int internalformat, int width, int height) {
+        GL30.glRenderbufferStorageMultisample(target, samples, internalformat, width, height);
+    }
+    
+    @Override
+    public void glBindFramebufferEXT(int param1, int param2) {
+        GL30.glBindFramebuffer(param1, param2);
+    }
+    
+    @Override
+    public void glBindRenderbufferEXT(int param1, int param2) {
+        GL30.glBindRenderbuffer(param1, param2);
+    }
+    
+    @Override
+    public int glCheckFramebufferStatusEXT(int param1) {
+        return GL30.glCheckFramebufferStatus(param1);
+    }
+    
+    @Override
+    public void glDeleteFramebuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        GL30.glDeleteFramebuffers(param1);
+    }
+    
+    @Override
+    public void glDeleteRenderbuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        GL30.glDeleteRenderbuffers(param1);
+    }
+    
+    @Override
+    public void glFramebufferRenderbufferEXT(int param1, int param2, int param3, int param4) {
+        GL30.glFramebufferRenderbuffer(param1, param2, param3, param4);
+    }
+    
+    @Override
+    public void glFramebufferTexture2DEXT(int param1, int param2, int param3, int param4, int param5) {
+        GL30.glFramebufferTexture2D(param1, param2, param3, param4, param5);
+    }
+    
+    @Override
+    public void glGenFramebuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        GL30.glGenFramebuffers(param1);
+    }
+    
+    @Override
+    public void glGenRenderbuffersEXT(IntBuffer param1) {
+        checkLimit(param1);
+        GL30.glGenRenderbuffers(param1);
+    }
+    
+    @Override
+    public void glGenerateMipmapEXT(int param1) {
+        GL30.glGenerateMipmap(param1);
+    }
+    
+    @Override
+    public void glRenderbufferStorageEXT(int param1, int param2, int param3, int param4) {
+        GL30.glRenderbufferStorage(param1, param2, param3, param4);
+    }
+}

+ 28 - 7
jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglContext.java

@@ -39,13 +39,18 @@ import com.jme3.renderer.Renderer;
 import com.jme3.renderer.RendererException;
 import com.jme3.renderer.lwjgl.LwjglGL;
 import com.jme3.renderer.lwjgl.LwjglGLExt;
+import com.jme3.renderer.lwjgl.LwjglGLFboEXT;
+import com.jme3.renderer.lwjgl.LwjglGLFboGL3;
 import com.jme3.renderer.opengl.GL;
 import com.jme3.renderer.opengl.GL2;
 import com.jme3.renderer.opengl.GL3;
+import com.jme3.renderer.opengl.GL4;
 import com.jme3.renderer.opengl.GLDebugDesktop;
 import com.jme3.renderer.opengl.GLExt;
 import com.jme3.renderer.opengl.GLFbo;
 import com.jme3.renderer.opengl.GLRenderer;
+import com.jme3.renderer.opengl.GLTiming;
+import com.jme3.renderer.opengl.GLTimingState;
 import com.jme3.renderer.opengl.GLTracer;
 import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext;
@@ -203,28 +208,44 @@ public abstract class LwjglContext implements JmeContext {
         }
         
         if (settings.getRenderer().equals(AppSettings.LWJGL_OPENGL2)
-                || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
+         || settings.getRenderer().equals(AppSettings.LWJGL_OPENGL3)) {
             GL gl = new LwjglGL();
-            GLFbo glfbo = new LwjglGLExt();
+            GLExt glext = new LwjglGLExt();
+            GLFbo glfbo;
+            
+            if (GLContext.getCapabilities().OpenGL30) {
+                glfbo = new LwjglGLFboGL3();
+            } else {
+                glfbo = new LwjglGLFboEXT();
+            }
             
             if (settings.getBoolean("GraphicsDebug")) {
-                gl    = new GLDebugDesktop(gl, glfbo);
+                gl    = new GLDebugDesktop(gl, glext, glfbo);
+                glext = (GLExt) gl;
                 glfbo = (GLFbo) gl;
             }
             
+            if (settings.getBoolean("GraphicsTiming")) {
+                GLTimingState timingState = new GLTimingState();
+                gl    = (GL) GLTiming.createGLTiming(gl, timingState, GL.class, GL2.class, GL3.class, GL4.class);
+                glext = (GLExt) GLTiming.createGLTiming(glext, timingState, GLExt.class);
+                glfbo = (GLFbo) GLTiming.createGLTiming(glfbo, timingState, GLFbo.class);
+            }
+                  
             if (settings.getBoolean("GraphicsTrace")) {
-                gl    = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class);
-                glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLExt.class);
+                gl    = (GL) GLTracer.createDesktopGlTracer(gl, GL.class, GL2.class, GL3.class, GL4.class);
+                glext = (GLExt) GLTracer.createDesktopGlTracer(glext, GLExt.class);
+                glfbo = (GLFbo) GLTracer.createDesktopGlTracer(glfbo, GLFbo.class);
             }
             
-            renderer = new GLRenderer(gl, glfbo);
+            renderer = new GLRenderer(gl, glext, glfbo);
             renderer.initialize();
         } else {
             throw new UnsupportedOperationException("Unsupported renderer: " + settings.getRenderer());
         }
         
         if (GLContext.getCapabilities().GL_ARB_debug_output && settings.getBoolean("GraphicsDebug")) {
-            ARBDebugOutput.glDebugMessageCallbackARB(new ARBDebugOutputCallback());
+            ARBDebugOutput.glDebugMessageCallbackARB(new ARBDebugOutputCallback(new LwjglGLDebugOutputHandler()));
         }
         
         renderer.setMainFrameBufferSrgb(settings.getGammaCorrection());

+ 78 - 0
jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglGLDebugOutputHandler.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2009-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.system.lwjgl;
+
+import java.util.HashMap;
+import org.lwjgl.opengl.ARBDebugOutput;
+import org.lwjgl.opengl.ARBDebugOutputCallback;
+
+class LwjglGLDebugOutputHandler implements ARBDebugOutputCallback.Handler {
+
+    private static final HashMap<Integer, String> constMap = new HashMap<Integer, String>();
+    private static final String MESSAGE_FORMAT = 
+            "[JME3] OpenGL debug message\r\n" +
+            "       ID: %d\r\n" +
+            "       Source: %s\r\n" +
+            "       Type: %s\r\n" +
+            "       Severity: %s\r\n" +
+            "       Message: %s";
+    
+    static {
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB, "API");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_APPLICATION_ARB, "APPLICATION");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_OTHER_ARB, "OTHER");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_SHADER_COMPILER_ARB, "SHADER_COMPILER");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_THIRD_PARTY_ARB, "THIRD_PARTY");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB, "WINDOW_SYSTEM");
+        
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB, "DEPRECATED_BEHAVIOR");
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_ERROR_ARB, "ERROR");
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB, "OTHER");
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PERFORMANCE_ARB, "PERFORMANCE");
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_PORTABILITY_ARB, "PORTABILITY");
+        constMap.put(ARBDebugOutput.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB, "UNDEFINED_BEHAVIOR");
+        
+        constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_HIGH_ARB, "HIGH");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_ARB, "MEDIUM");
+        constMap.put(ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, "LOW");
+    }
+    
+    @Override
+    public void handleMessage(int source, int type, int id, int severity, String message) {
+        String sourceStr = constMap.get(source);
+        String typeStr = constMap.get(type);
+        String severityStr = constMap.get(severity);
+        
+        System.err.println(String.format(MESSAGE_FORMAT, id, sourceStr, typeStr, severityStr, message));
+    }
+    
+}

+ 8 - 0
jme3-networking/src/main/java/com/jme3/network/Client.java

@@ -31,6 +31,8 @@
  */
 package com.jme3.network;
 
+import com.jme3.network.service.ClientServiceManager;
+
 
 /**
  *  Represents a remote connection to a server that can be used
@@ -72,6 +74,12 @@ public interface Client extends MessageConnection
      *  be able to connect to.
      */   
     public int getVersion();
+
+    /**
+     *  Returns the manager for client services.  Client services extend
+     *  the functionality of the client.
+     */
+    public ClientServiceManager getServices();     
  
     /**
      *  Sends a message to the server.

+ 8 - 0
jme3-networking/src/main/java/com/jme3/network/Server.java

@@ -33,6 +33,8 @@ package com.jme3.network;
 
 import java.util.Collection;
 
+import com.jme3.network.service.HostedServiceManager;
+
 /**
  *  Represents a host that can send and receive messages to
  *  a set of remote client connections.
@@ -54,6 +56,12 @@ public interface Server
      */   
     public int getVersion();
 
+    /**
+     *  Returns the manager for hosted services.  Hosted services extend
+     *  the functionality of the server.
+     */
+    public HostedServiceManager getServices();     
+
     /**
      *  Sends the specified message to all connected clients.
      */ 

+ 27 - 2
jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java

@@ -37,6 +37,8 @@ import com.jme3.network.kernel.Connector;
 import com.jme3.network.message.ChannelInfoMessage;
 import com.jme3.network.message.ClientRegistrationMessage;
 import com.jme3.network.message.DisconnectMessage;
+import com.jme3.network.service.ClientServiceManager;
+import com.jme3.network.service.serializer.ClientSerializerRegistrationsService;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.*;
@@ -54,7 +56,7 @@ import java.util.logging.Logger;
  */
 public class DefaultClient implements Client
 {
-    static Logger log = Logger.getLogger(DefaultClient.class.getName());
+    static final Logger log = Logger.getLogger(DefaultClient.class.getName());
     
     // First two channels are reserved for reliable and
     // unreliable.  Note: channels are endpoint specific so these
@@ -80,10 +82,14 @@ public class DefaultClient implements Client
  
     private ConnectorFactory connectorFactory;
     
+    private ClientServiceManager services;
+    
     public DefaultClient( String gameName, int version )
     {
         this.gameName = gameName;
         this.version = version;
+        this.services = new ClientServiceManager(this);
+        addStandardServices();
     }
     
     public DefaultClient( String gameName, int version, Connector reliable, Connector fast,
@@ -93,6 +99,10 @@ public class DefaultClient implements Client
         setPrimaryConnectors( reliable, fast, connectorFactory );
     }
 
+    protected void addStandardServices() {
+        services.addService(new ClientSerializerRegistrationsService());
+    }
+
     protected void setPrimaryConnectors( Connector reliable, Connector fast, ConnectorFactory connectorFactory )
     {
         if( reliable == null )
@@ -200,6 +210,11 @@ public class DefaultClient implements Client
     {
         return version;
     }
+    
+    public ClientServiceManager getServices() 
+    {
+        return services;
+    }
    
     public void send( Message message )
     {
@@ -260,7 +275,7 @@ public class DefaultClient implements Client
     {
         checkRunning();
  
-        closeConnections( null );            
+        closeConnections( null );
     }         
 
     protected void closeConnections( DisconnectInfo info )
@@ -268,6 +283,10 @@ public class DefaultClient implements Client
         if( !isRunning )
             return;
 
+        // Let the services get a chance to stop before we
+        // kill the connection.
+        services.stop();
+        
         // Send a close message
     
         // Tell the thread it's ok to die
@@ -285,6 +304,9 @@ public class DefaultClient implements Client
         fireDisconnected(info);
         
         isRunning = false;
+        
+        // Terminate the services
+        services.terminate();            
     }         
 
     public void addClientStateListener( ClientStateListener listener )
@@ -329,6 +351,9 @@ public class DefaultClient implements Client
  
     protected void fireConnected()
     {
+        // Let the services know we are finally started
+        services.start();
+    
         for( ClientStateListener l : stateListeners ) {
             l.clientConnected( this );
         }            

+ 53 - 5
jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java

@@ -37,6 +37,8 @@ import com.jme3.network.kernel.Kernel;
 import com.jme3.network.message.ChannelInfoMessage;
 import com.jme3.network.message.ClientRegistrationMessage;
 import com.jme3.network.message.DisconnectMessage;
+import com.jme3.network.service.HostedServiceManager;
+import com.jme3.network.service.serializer.ServerSerializerRegistrationsService;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.*;
@@ -55,7 +57,7 @@ import java.util.logging.Logger;
  */
 public class DefaultServer implements Server
 {
-    static Logger log = Logger.getLogger(DefaultServer.class.getName());
+    static final Logger log = Logger.getLogger(DefaultServer.class.getName());
 
     // First two channels are reserved for reliable and
     // unreliable
@@ -85,6 +87,8 @@ public class DefaultServer implements Server
                             = new MessageListenerRegistry<HostedConnection>();                        
     private List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>();
     
+    private HostedServiceManager services;
+    
     public DefaultServer( String gameName, int version, Kernel reliable, Kernel fast )
     {
         if( reliable == null )
@@ -92,6 +96,8 @@ public class DefaultServer implements Server
             
         this.gameName = gameName;
         this.version = version;
+        this.services = new HostedServiceManager(this);        
+        addStandardServices();
         
         reliableAdapter = new KernelAdapter( this, reliable, dispatcher, true );
         channels.add( reliableAdapter );
@@ -101,6 +107,10 @@ public class DefaultServer implements Server
         }
     }   
 
+    protected void addStandardServices() {
+        services.addService(new ServerSerializerRegistrationsService());
+    }
+
     public String getGameName()
     {
         return gameName;
@@ -110,6 +120,11 @@ public class DefaultServer implements Server
     {
         return version;
     }
+    
+    public HostedServiceManager getServices() 
+    {
+        return services;
+    }
 
     public int addChannel( int port )
     {
@@ -164,7 +179,10 @@ public class DefaultServer implements Server
             ka.start();
         }
         
-        isRunning = true;             
+        isRunning = true;
+        
+        // Start the services
+        services.start();             
     }
 
     public boolean isRunning()
@@ -177,13 +195,20 @@ public class DefaultServer implements Server
         if( !isRunning )
             throw new IllegalStateException( "Server is not started." );
  
+        // First stop the services since we are about to
+        // kill the connections they are using
+        services.stop();
+ 
         try {
             // Kill the adpaters, they will kill the kernels
             for( KernelAdapter ka : channels ) {
                 ka.close();
             }
             
-            isRunning = false;            
+            isRunning = false;
+            
+            // Now terminate all of the services
+            services.terminate();             
         } catch( InterruptedException e ) {
             throw new RuntimeException( "Interrupted while closing", e );
         }                               
@@ -396,6 +421,18 @@ public class DefaultServer implements Server
         return endpointConnections.get(endpoint);       
     } 
 
+    protected void removeConnecting( Endpoint p ) 
+    {
+        // No easy lookup for connecting Connections
+        // from endpoint.
+        for( Map.Entry<Long,Connection> e : connecting.entrySet() ) {
+            if( e.getValue().hasEndpoint(p) ) {
+                connecting.remove(e.getKey());
+                return;
+            } 
+        }
+    }
+
     protected void connectionClosed( Endpoint p )
     {
         if( p.isConnected() ) {
@@ -411,10 +448,10 @@ public class DefaultServer implements Server
         // Also note: this method will be called multiple times per
         // HostedConnection if it has multiple endpoints.
  
-        Connection removed = null;
+        Connection removed;
         synchronized( this ) {             
             // Just in case the endpoint was still connecting
-            connecting.values().remove(p);
+            removeConnecting(p);
 
             // And the regular management
             removed = (Connection)endpointConnections.remove(p);
@@ -452,6 +489,16 @@ public class DefaultServer implements Server
             id = nextId.getAndIncrement();
             channels = new Endpoint[channelCount];
         }
+        
+        boolean hasEndpoint( Endpoint p )
+        {
+            for( Endpoint e : channels ) {
+                if( p == e ) {
+                    return true;
+                }
+            }
+            return false;
+        }
  
         void setChannel( int channel, Endpoint p )
         {
@@ -557,6 +604,7 @@ public class DefaultServer implements Server
             return Collections.unmodifiableSet(sessionData.keySet());
         }           
         
+        @Override
         public String toString()
         {
             return "Connection[ id=" + id + ", reliable=" + channels[CH_RELIABLE] 

+ 188 - 0
jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java

@@ -0,0 +1,188 @@
+/*
+ * $Id: SerializerRegistrationsMessage.java 3829 2014-11-24 07:25:43Z pspeed $
+ *
+ * Copyright (c) 2012, Paul Speed
+ * All rights reserved.
+ */
+
+package com.jme3.network.message;
+
+import com.jme3.network.AbstractMessage;
+import com.jme3.network.serializing.Serializable;
+import com.jme3.network.serializing.Serializer;
+import com.jme3.network.serializing.SerializerRegistration;
+import com.jme3.network.serializing.serializers.FieldSerializer;
+import java.util.*;
+import java.util.jar.Attributes;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ *  Holds a compiled set of message registration information that
+ *  can be sent over the wire.  The received message can then be
+ *  used to register all of the classes using the same IDs and 
+ *  same ordering, etc..  The intent is that the server compiles
+ *  this message once it is sure that all serializable classes have
+ *  been registered.  It can then send this to each new client and
+ *  they can use it to register all of the classes without requiring
+ *  exactly reproducing the same calls that the server did to register
+ *  messages.
+ *
+ *  <p>Normally, JME recommends that apps have a common utility method
+ *  that they call on both client and server.  However, this makes 
+ *  pluggable services nearly impossible as some central class has to
+ *  know about all registered serializers.  This message implementation
+ *  gets around by only requiring registration on the server.</p>
+ *
+ *  @author    Paul Speed
+ */
+@Serializable
+public class SerializerRegistrationsMessage extends AbstractMessage {
+
+    static final Logger log = Logger.getLogger(SerializerRegistrationsMessage.class.getName());
+
+    public static final Set<Class> ignore = new HashSet<Class>();
+    static {
+        // We could build this automatically but then we
+        // risk making a client and server out of date simply because
+        // their JME versions are out of date.
+        ignore.add(Boolean.class);
+        ignore.add(Float.class);
+        ignore.add(Boolean.class);
+        ignore.add(Byte.class);
+        ignore.add(Character.class);
+        ignore.add(Short.class);
+        ignore.add(Integer.class);
+        ignore.add(Long.class);
+        ignore.add(Float.class);
+        ignore.add(Double.class);
+        ignore.add(String.class);
+ 
+        ignore.add(DisconnectMessage.class);
+        ignore.add(ClientRegistrationMessage.class);
+    
+        ignore.add(Date.class);
+        ignore.add(AbstractCollection.class);
+        ignore.add(AbstractList.class);
+        ignore.add(AbstractSet.class);
+        ignore.add(ArrayList.class);
+        ignore.add(HashSet.class);
+        ignore.add(LinkedHashSet.class);
+        ignore.add(LinkedList.class);
+        ignore.add(TreeSet.class);
+        ignore.add(Vector.class);
+        ignore.add(AbstractMap.class);
+        ignore.add(Attributes.class);
+        ignore.add(HashMap.class);
+        ignore.add(Hashtable.class);
+        ignore.add(IdentityHashMap.class);
+        ignore.add(TreeMap.class);
+        ignore.add(WeakHashMap.class);        
+        ignore.add(Enum.class);
+        
+        ignore.add(GZIPCompressedMessage.class);
+        ignore.add(ZIPCompressedMessage.class);
+
+        ignore.add(ChannelInfoMessage.class);
+        
+        ignore.add(SerializerRegistrationsMessage.class);
+        ignore.add(SerializerRegistrationsMessage.Registration.class);        
+    }
+ 
+    public static SerializerRegistrationsMessage INSTANCE;   
+    public static Registration[] compiled;
+    private static final Serializer fieldSerializer = new FieldSerializer();
+    
+    private Registration[] registrations;
+
+    public SerializerRegistrationsMessage() {
+        setReliable(true);
+    }
+
+    public SerializerRegistrationsMessage( Registration... registrations ) {
+        setReliable(true);
+        this.registrations = registrations;
+    }
+    
+    public static void compile() {
+    
+        // Let's just see what they are here
+        List<Registration> list = new ArrayList<Registration>();
+        for( SerializerRegistration reg : Serializer.getSerializerRegistrations() ) {
+            Class type = reg.getType();
+            if( ignore.contains(type) )
+                continue;
+            if( type.isPrimitive() )
+                continue;
+ 
+            list.add(new Registration(reg));
+        }
+            
+        if( log.isLoggable(Level.FINE) ) {
+            log.log( Level.FINE, "Number of registered classes:{0}", list.size());
+            for( Registration reg : list ) { 
+                log.log( Level.FINE, "    {0}", reg);
+            }
+        }
+        compiled = list.toArray(new Registration[list.size()]);
+        
+        INSTANCE = new SerializerRegistrationsMessage(compiled);                                
+    }
+ 
+    public void registerAll() {    
+        for( Registration reg : registrations ) {
+            log.log( Level.INFO, "Registering:{0}", reg);
+            reg.register();
+        }
+    }
+    
+    @Serializable
+    public static final class Registration {
+    
+        private short id;
+        private String className;
+        private String serializerClassName;
+        
+        public Registration() {
+        }
+        
+        public Registration( SerializerRegistration reg ) {
+        
+            this.id = reg.getId();
+            this.className = reg.getType().getName();
+            if( reg.getSerializer().getClass() != FieldSerializer.class ) {
+                this.serializerClassName = reg.getSerializer().getClass().getName();
+            } 
+        }
+ 
+        public void register() {        
+            try {
+                Class type = Class.forName(className);
+                Serializer serializer;
+                if( serializerClassName == null ) {
+                    serializer = fieldSerializer;
+                } else {
+                    Class serializerType = Class.forName(serializerClassName);
+                    serializer = (Serializer)serializerType.newInstance();                    
+                }
+                SerializerRegistration result = Serializer.registerClassForId(id, type, serializer);
+                log.log( Level.FINE, "   result:{0}", result);                
+            } catch( ClassNotFoundException e ) {
+                throw new RuntimeException( "Class not found attempting to register:" + this, e );
+            } catch( InstantiationException e ) {
+                throw new RuntimeException( "Error instantiating serializer registering:" + this, e );
+            } catch( IllegalAccessException e ) {
+                throw new RuntimeException( "Error instantiating serializer registering:" + this, e );
+            }            
+        }
+        
+        @Override
+        public String toString() {
+            return "Registration[" + id + " = " + className + ", serializer=" + serializerClassName + "]";
+        }
+    }
+}
+
+
+

+ 51 - 0
jme3-networking/src/main/java/com/jme3/network/service/AbstractClientService.java

@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+
+/**
+ *  Convenient base class for ClientServices providing some default ClientService 
+ *  interface implementations as well as a few convenience methods 
+ *  such as getServiceManager() and getService(type).  Subclasses 
+ *  must at least override the onInitialize() method to handle 
+ *  service initialization.
+ *
+ *  @author    Paul Speed
+ */
+public abstract class AbstractClientService extends AbstractService<ClientServiceManager> 
+                                            implements ClientService { 
+    
+    protected AbstractClientService() {
+    }
+   
+}

+ 70 - 0
jme3-networking/src/main/java/com/jme3/network/service/AbstractHostedService.java

@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import com.jme3.network.HostedConnection;
+import com.jme3.network.Server;
+
+
+/**
+ *  Convenient base class for HostedServices providing some default HostedService 
+ *  interface implementations as well as a few convenience methods 
+ *  such as getServiceManager() and getService(type).  Subclasses 
+ *  must at least override the onInitialize() method to handle 
+ *  service initialization.
+ *
+ *  @author    Paul Speed
+ */
+public abstract class AbstractHostedService extends AbstractService<HostedServiceManager> 
+                                            implements HostedService { 
+    
+    protected AbstractHostedService() {
+    }
+
+    /**
+     *  Default implementation does nothing.  Implementations can
+     *  override this to peform custom new connection behavior.
+     */
+    @Override
+    public void connectionAdded(Server server, HostedConnection hc) {
+    }
+
+    /**
+     *  Default implementation does nothing.  Implementations can
+     *  override this to peform custom leaving connection behavior.
+     */
+    @Override
+    public void connectionRemoved(Server server, HostedConnection hc) {
+    }
+    
+}

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

@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.network.service;
+
+
+/**
+ *  Base class providing some default Service interface implementations
+ *  as well as a few convenience methods such as getServiceManager()
+ *  and getService(type).  Subclasses must at least override the
+ *  onInitialize() method to handle service initialization.
+ *
+ *  @author    Paul Speed
+ */
+public abstract class AbstractService<S extends ServiceManager> implements Service<S> {
+    
+    private S serviceManager;
+ 
+    protected AbstractService() {
+    }
+    
+    /**
+     *  Returns the ServiceManager that was passed to
+     *  initialize() during service initialization.
+     */
+    protected S getServiceManager() {
+        return serviceManager;
+    }
+ 
+    /**
+     *  Retrieves the first sibling service of the specified
+     *  type.
+     */   
+    protected <T extends Service<S>> T getService( Class<T> type ) {
+        return type.cast(serviceManager.getService(type));
+    }    
+    
+    /**
+     *  Initializes this service by keeping a reference to
+     *  the service manager and calling onInitialize().
+     */
+    @Override
+    public final void initialize( S serviceManager ) {
+        this.serviceManager = serviceManager;
+        onInitialize(serviceManager);
+    }
+ 
+    /**
+     *  Called during initialize() for the subclass to perform
+     *  implementation specific initialization.
+     */   
+    protected abstract void onInitialize( S serviceManager );
+    
+    /**
+     *  Default implementation does nothing.  Implementations can
+     *  override this to peform custom startup behavior.
+     */
+    @Override
+    public void start() {    
+    }
+    
+    /**
+     *  Default implementation does nothing.  Implementations can
+     *  override this to peform custom stop behavior.
+     */
+    @Override
+    public void stop() {    
+    }
+    
+    /**
+     *  Default implementation does nothing.  Implementations can
+     *  override this to peform custom termination behavior.
+     */
+    @Override
+    public void terminate( S serviceManager ) {
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getName() + "[serviceManager=" + serviceManager + "]";
+    }
+}

+ 72 - 0
jme3-networking/src/main/java/com/jme3/network/service/ClientService.java

@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+
+/**
+ *  Interface implemented by Client-side services that augment
+ *  a network Client's functionality. 
+ *
+ *  @author    Paul Speed
+ */
+public interface ClientService extends Service<ClientServiceManager> {
+    
+    /**
+     *  Called when the service is first attached to the service
+     *  manager.
+     */
+    @Override
+    public void initialize( ClientServiceManager serviceManager );
+    
+    /**
+     *  Called when the service manager is started or if the 
+     *  service is added to an already started service manager.
+     */
+    @Override 
+    public void start();
+    
+    /**
+     *  Called when the service is shutting down.  All services
+     *  are stopped and any service manager resources are closed
+     *  before the services are terminated.
+     */
+    @Override
+    public void stop();
+    
+    /**
+     *  The service manager is fully shutting down.  All services
+     *  have been stopped and related connections closed.
+     */
+    @Override
+    public void terminate( ClientServiceManager serviceManager ); 
+}

+ 100 - 0
jme3-networking/src/main/java/com/jme3/network/service/ClientServiceManager.java

@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import com.jme3.network.Client;
+
+
+/**
+ *  Manages ClientServices on behalf of a network Client object.
+ *
+ *  @author    Paul Speed
+ */
+public class ClientServiceManager extends ServiceManager<ClientServiceManager> {
+    
+    private Client client;
+ 
+    /**
+     *  Creates a new ClientServiceManager for the specified network Client.
+     */   
+    public ClientServiceManager( Client client ) {
+        this.client = client;
+    }
+ 
+    /**
+     *  Returns the network Client associated with this ClientServiceManager.
+     */   
+    public Client getClient() {
+        return client;
+    }
+
+    /**
+     *  Returns 'this' and is what is passed to ClientService.initialize()
+     *  and ClientService.termnate();
+     */
+    @Override
+    protected final ClientServiceManager getParent() {
+        return this;
+    }
+    
+    /**
+     *  Adds the specified ClientService and initializes it.  If the service manager
+     *  has already been started then the service will also be started.
+     */   
+    public void addService( ClientService s ) {
+        super.addService(s);
+    }
+
+    /**
+     *  Adds all of the specified ClientServices and initializes them.  If the service manager
+     *  has already been started then the services will also be started.
+     *  This is a convenience method that delegates to addService(), thus each
+     *  service will be initialized (and possibly started) in sequence rather
+     *  than doing them all at the end.
+     */   
+    public void addServices( ClientService... services ) {
+        for( ClientService s : services ) {
+            super.addService(s);
+        }
+    }
+
+    /**
+     *  Removes the specified ClientService from this service manager, stopping
+     *  and terminating it as required.  If this service manager is in a
+     *  started state then the service will be stopped.  After removal,
+     *  the service will be terminated.
+     */
+    public void removeService( ClientService s ) {
+        super.removeService(s);
+    }
+}

+ 74 - 0
jme3-networking/src/main/java/com/jme3/network/service/HostedService.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 com.jme3.network.service;
+
+import com.jme3.network.ConnectionListener;
+
+
+/**
+ *  Interface implemented by Server-side services that augment
+ *  a network Server's functionality. 
+ *
+ *  @author    Paul Speed
+ */
+public interface HostedService extends Service<HostedServiceManager>, ConnectionListener {
+    
+    /**
+     *  Called when the service is first attached to the service
+     *  manager.
+     */
+    @Override
+    public void initialize( HostedServiceManager serviceManager );
+    
+    /**
+     *  Called when the service manager is started or if the 
+     *  service is added to an already started service manager.
+     */
+    @Override 
+    public void start();
+    
+    /**
+     *  Called when the service is shutting down.  All services
+     *  are stopped and any service manager resources are closed
+     *  before the services are terminated.
+     */
+    @Override
+    public void stop();
+    
+    /**
+     *  The service manager is fully shutting down.  All services
+     *  have been stopped and related connections closed.
+     */
+    @Override
+    public void terminate( HostedServiceManager serviceManager ); 
+}

+ 140 - 0
jme3-networking/src/main/java/com/jme3/network/service/HostedServiceManager.java

@@ -0,0 +1,140 @@
+/*
+ * 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;
+
+import com.jme3.network.ConnectionListener;
+import com.jme3.network.HostedConnection;
+import com.jme3.network.Server;
+
+
+/**
+ *  Manages HostedServices on behalf of a network Server object.
+ *  All HostedServices are automatically informed about new and 
+ *  leaving connections.
+ *
+ *  @author    Paul Speed
+ */
+public class HostedServiceManager extends ServiceManager<HostedServiceManager> {
+    
+    private Server server;
+    private ConnectionObserver connectionObserver;
+
+    /**
+     *  Creates a HostedServiceManager for the specified network Server.
+     */    
+    public HostedServiceManager( Server server ) {
+        this.server = server;
+        this.connectionObserver = new ConnectionObserver();
+        server.addConnectionListener(connectionObserver);
+    }
+
+    /**
+     *  Returns the network Server associated with this HostedServiceManager.
+     */
+    public Server getServer() {
+        return server;
+    }
+
+    /**
+     *  Returns 'this' and is what is passed to HostedService.initialize()
+     *  and HostedService.termnate();
+     */
+    @Override
+    protected final HostedServiceManager getParent() {
+        return this;
+    }
+ 
+    /**
+     *  Adds the specified HostedService and initializes it.  If the service manager
+     *  has already been started then the service will also be started.
+     */   
+    public void addService( HostedService s ) {
+        super.addService(s);
+    }
+
+    /**
+     *  Adds all of the specified HostedServices and initializes them.  If the service manager
+     *  has already been started then the services will also be started.
+     *  This is a convenience method that delegates to addService(), thus each
+     *  service will be initialized (and possibly started) in sequence rather
+     *  than doing them all at the end.
+     */   
+    public void addServices( HostedService... services ) {
+        for( HostedService s : services ) {
+            super.addService(s);
+        }
+    }
+
+    /**
+     *  Removes the specified HostedService from this service manager, stopping
+     *  and terminating it as required.  If this service manager is in a
+     *  started state then the service will be stopped.  After removal,
+     *  the service will be terminated.
+     */
+    public void removeService( HostedService s ) {
+        super.removeService(s);
+    }
+    
+    /**
+     *  Called internally when a new connection has been added so that the
+     *  services can be notified.
+     */
+    protected void addConnection( HostedConnection hc ) {
+        for( Service s : getServices() ) {
+            ((HostedService)s).connectionAdded(server, hc);
+        }
+    }
+ 
+    /**
+     *  Called internally when a connection has been removed so that the
+     *  services can be notified.
+     */   
+    protected void removeConnection( HostedConnection hc ) {
+        for( Service s : getServices() ) {
+            ((HostedService)s).connectionRemoved(server, hc);
+        }
+    }
+ 
+    protected class ConnectionObserver implements ConnectionListener {
+
+        @Override
+        public void connectionAdded(Server server, HostedConnection hc) {
+            addConnection(hc);
+        }
+
+        @Override
+        public void connectionRemoved(Server server, HostedConnection hc) {
+            removeConnection(hc);
+        }
+    }
+}

+ 67 - 0
jme3-networking/src/main/java/com/jme3/network/service/Service.java

@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+
+/**
+ *  The base interface for managed services.
+ *
+ *  @author    Paul Speed
+ */
+public interface Service<S> {
+    
+    /**
+     *  Called when the service is first attached to the service
+     *  manager.
+     */
+    public void initialize( S serviceManager );
+    
+    /**
+     *  Called when the service manager is started or if the 
+     *  service is added to an already started service manager.
+     */
+    public void start();
+    
+    /**
+     *  Called when the service manager is shutting down.  All services
+     *  are stopped and any service manager resources are closed
+     *  before the services are terminated.
+     */
+    public void stop();
+    
+    /**
+     *  The service manager is fully shutting down.  All services
+     *  have been stopped and related connections closed.
+     */
+    public void terminate( S serviceManager ); 
+}

+ 160 - 0
jme3-networking/src/main/java/com/jme3/network/service/ServiceManager.java

@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ *  The base service manager class from which the HostedServiceManager
+ *  and ClientServiceManager classes are derived.  This manages the
+ *  the underlying services and their life cycles.
+ *
+ *  @author    Paul Speed
+ */
+public abstract class ServiceManager<T> {
+
+    private List<Service<T>> services = new CopyOnWriteArrayList<Service<T>>();
+    private volatile boolean started = false;
+    
+    protected ServiceManager() {
+    }
+
+    /**
+     *  Retreives the 'parent' of this service manager, usually
+     *  a more specifically typed version of 'this' but it can be
+     *  anything the seervices are expecting.
+     */
+    protected abstract T getParent();
+ 
+    /**
+     *  Returns the complete list of services managed by this
+     *  service manager.  This list is thread safe following the
+     *  CopyOnWriteArrayList semantics.
+     */   
+    protected List<Service<T>> getServices() {
+        return services;
+    }
+    
+    /**
+     *  Starts this service manager and all services that it contains.
+     *  Any services added after the service manager has started will have
+     *  their start() methods called.
+     */   
+    public void start() {
+        if( started ) {
+            return;
+        }
+        for( Service<T> s : services ) {
+            s.start();
+        }
+        started = true;
+    }
+
+    /**
+     *  Returns true if this service manager has been started.
+     */
+    public boolean isStarted() {
+        return started;
+    }
+ 
+    /**
+     *  Stops all services and puts the service manager into a stopped state.
+     */   
+    public void stop() {
+        if( !started ) {
+            throw new IllegalStateException(getClass().getSimpleName() + " not started.");
+        }
+        for( Service<T> s : services ) {
+            s.stop();
+        }
+        started = false;
+    }
+ 
+    /**
+     *  Adds the specified service and initializes it.  If the service manager
+     *  has already been started then the service will also be started.
+     */   
+    public <S extends Service<T>> void addService( S s ) {
+        services.add(s);
+        s.initialize(getParent());
+        if( started ) {
+            s.start();
+        }
+    }
+
+    /**
+     *  Removes the specified service from this service manager, stopping
+     *  and terminating it as required.  If this service manager is in a
+     *  started state then the service will be stopped.  After removal,
+     *  the service will be terminated.
+     */
+    public <S extends Service<T>> void removeService( S s ) {
+        if( started ) {
+            s.stop();
+        }
+        services.remove(s);
+        s.terminate(getParent());        
+    }
+ 
+    /**
+     *  Terminates all services.  If the service manager has not been
+     *  stopped yet then it will be stopped.
+     */
+    public void terminate() {
+        if( started ) {
+            stop();
+        }
+        for( Service<T> s : services ) {
+            s.terminate(getParent());
+        }               
+    }
+ 
+    /**
+     *  Retrieves the first service of the specified type.
+     */    
+    public <S extends Service<T>> S getService( Class<S> type ) {
+        for( Service s : services ) {
+            if( type.isInstance(s) ) {
+                return type.cast(s);
+            }
+        }
+        return null;
+    }
+ 
+    @Override   
+    public String toString() {
+        return getClass().getName() + "[services=" + services + "]";
+    }
+}
+

+ 123 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcClientService.java

@@ -0,0 +1,123 @@
+/*
+ * 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.rpc;
+
+import com.jme3.network.Client;
+import com.jme3.network.util.ObjectMessageDelegator;
+import com.jme3.network.service.AbstractClientService;
+import com.jme3.network.service.ClientServiceManager;
+
+
+/**
+ *  RPC service that can be added to a network Client to
+ *  add RPC send/receive capabilities.  Remote procedure
+ *  calls can be made to the server and responses retrieved.
+ *  Any remote procedure calls that the server performs for
+ *  this connection will be received by this service and delegated
+ *  to the appropriate RpcHandlers. 
+ *
+ *  @author    Paul Speed
+ */
+public class RpcClientService extends AbstractClientService {
+
+    private RpcConnection rpc;
+    private ObjectMessageDelegator delegator;
+
+    /**
+     *  Creates a new RpcClientService that can be registered
+     *  with the network Client object.
+     */
+    public RpcClientService() {
+    }
+
+    /**
+     *  Used internally to setup the RpcConnection and MessageDelegator.
+     */
+    @Override
+    protected void onInitialize( ClientServiceManager serviceManager ) {        
+        Client client = serviceManager.getClient();
+        this.rpc = new RpcConnection(client);
+        
+        delegator = new ObjectMessageDelegator(rpc, true);       
+        client.addMessageListener(delegator, delegator.getMessageTypes());                   
+    }
+
+    /**
+     *  Used internally to unregister the RPC MessageDelegator that
+     *  was previously added to the network Client.
+     */
+    @Override
+    public void terminate( ClientServiceManager serviceManager ) {
+        Client client = serviceManager.getClient();
+        client.removeMessageListener(delegator, delegator.getMessageTypes());                   
+    }
+ 
+    /**
+     *  Performs a synchronous call on the server against the specified
+     *  object using the specified procedure ID.  Both inboud and outbound
+     *  communication is done on the specified channel.
+     */
+    public Object callAndWait( byte channel, short objId, short procId, Object... args ) {
+        return rpc.callAndWait(channel, objId, procId, args);
+    }
+
+    /**
+     *  Performs an asynchronous call on the server against the specified
+     *  object using the specified procedure ID.  Communication is done
+     *  over the specified channel.  No responses are received and none
+     *  are waited for.
+     */
+    public void callAsync( byte channel, short objId, short procId, Object... args ) {
+        rpc.callAsync(channel, objId, procId, args);
+    }
+ 
+    /** 
+     *  Register a handler that will be called when the server
+     *  performs a remove procedure call against this client. 
+     *  Only one handler per object ID can be registered at any given time,
+     *  though the same handler can be registered for multiple object
+     *  IDs.
+     */    
+    public void registerHandler( short objId, RpcHandler handler ) {
+        rpc.registerHandler(objId, handler);
+    }
+ 
+    /**
+     *  Removes a previously registered handler for the specified
+     *  object ID.  
+     */
+    public void removeHandler( short objId, RpcHandler handler ) {
+        rpc.removeHandler(objId, handler);
+    }
+
+}

+ 260 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java

@@ -0,0 +1,260 @@
+/*
+ * 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.rpc;
+
+import com.jme3.network.MessageConnection;
+import com.jme3.network.service.rpc.msg.RpcCallMessage;
+import com.jme3.network.service.rpc.msg.RpcResponseMessage;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ *  Wraps a message connection to provide RPC call support.  This
+ *  is used internally by the RpcClientService and RpcHostedService to manage
+ *  network messaging.
+ *
+ *  @author    Paul Speed
+ */
+public class RpcConnection {
+
+    static final Logger log = Logger.getLogger(RpcConnection.class.getName());
+ 
+    /**
+     *  The underlying connection upon which RPC call messages are sent
+     *  and RPC response messages are received.  It can be a Client or
+     *  a HostedConnection depending on the mode of the RPC service.
+     */
+    private MessageConnection connection;
+    
+    /**
+     *  The objectId index of RpcHandler objects that are used to perform the
+     *  RPC calls for a particular object.
+     */
+    private Map<Short, RpcHandler> handlers = new ConcurrentHashMap<Short, RpcHandler>();
+    
+    /**
+     *  Provides unique messages IDs for outbound synchronous call
+     *  messages.  These are then used in the responses index to
+     *  locate the proper ResponseHolder objects.
+     */
+    private AtomicLong sequenceNumber = new AtomicLong();
+    
+    /**
+     *  Tracks the ResponseHolder objects for sent message IDs.  When the
+     *  response is received, the appropriate handler is found here and the
+     *  response or error set, thus releasing the waiting caller.
+     */ 
+    private Map<Long, ResponseHolder> responses = new ConcurrentHashMap<Long, ResponseHolder>(); 
+ 
+    /**
+     *  Creates a new RpcConnection for the specified network connection.
+     */   
+    public RpcConnection( MessageConnection connection ) {
+        this.connection = connection;
+    }
+ 
+    /**
+     *  Clears any pending synchronous calls causing them to
+     *  throw an exception with the message "Closing connection".
+     */    
+    public void close() {
+        // Let any pending waits go free
+        for( ResponseHolder holder : responses.values() ) {
+            holder.release();
+        }
+    }
+ 
+    /**
+     *  Performs a remote procedure call with the specified arguments and waits
+     *  for the response.  Both the outbound message and inbound response will
+     *  be sent on the specified channel.
+     */
+    public Object callAndWait( byte channel, short objId, short procId, Object... args ) {
+        
+        RpcCallMessage msg = new RpcCallMessage(sequenceNumber.getAndIncrement(), 
+                                                channel, objId, procId, args);
+        
+        // Need to register an object so we can wait for the response.
+        // ...before we send it.  Just in case.
+        ResponseHolder holder = new ResponseHolder(msg); 
+        responses.put(msg.getMessageId(), holder);        
+ 
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "Sending:{0}  on channel:{1}", new Object[]{msg, channel});
+        }        
+        if( channel >= 0 ) {
+            connection.send(channel, msg);
+        } else {
+            connection.send(msg);
+        }
+                
+        return holder.getResponse();
+    }
+
+    /**
+     *  Performs a remote procedure call with the specified arguments but does
+     *  not wait for a response.  The outbound message is sent on the specified channel.
+     *  There is no inbound response message. 
+     */
+    public void callAsync( byte channel, short objId, short procId, Object... args ) {
+        
+        RpcCallMessage msg = new RpcCallMessage(-1, channel, objId, procId, args);
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "Sending:{0}  on channel:{1}", new Object[]{msg, channel});
+        }        
+        connection.send(channel, msg);        
+    }
+    
+    /** 
+     *  Register a handler that can be called by the other end
+     *  of the connection using the specified object ID.  Only one
+     *  handler per object ID can be registered at any given time,
+     *  though the same handler can be registered for multiple object
+     *  IDs.
+     */    
+    public void registerHandler( short objId, RpcHandler handler ) {
+        handlers.put(objId, handler);
+    }
+    
+    /**
+     *  Removes a previously registered handler for the specified
+     *  object ID.  
+     */
+    public void removeHandler( short objId, RpcHandler handler ) {
+        RpcHandler removing = handlers.get(objId);
+        if( handler != removing ) {
+            throw new IllegalArgumentException("Handler not registered for object ID:" 
+                                                + objId + ", handler:" + handler );
+        }
+        handlers.remove(objId);
+    }
+ 
+    protected void send( byte channel, RpcResponseMessage msg ) {
+        if( channel >= 0 ) {
+            connection.send(channel, msg);
+        } else {
+            connection.send(msg);
+        }
+    }
+ 
+    /**
+     *  Called internally when an RpcCallMessage is received from 
+     *  the remote connection.
+     */ 
+    public void handleMessage( RpcCallMessage msg ) {
+    
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "handleMessage({0})", msg);
+        }    
+        RpcHandler handler = handlers.get(msg.getObjectId());
+        try {
+            if( handler == null ) {
+                throw new RuntimeException("Handler not found for objectID:" + msg.getObjectId());
+            }
+            Object result = handler.call(this, msg.getObjectId(), msg.getProcedureId(), msg.getArguments());
+            if( !msg.isAsync() ) {
+                send(msg.getChannel(), new RpcResponseMessage(msg.getMessageId(), result));
+            }
+        } catch( Exception e ) {
+            if( !msg.isAsync() ) {
+                send(msg.getChannel(), new RpcResponseMessage(msg.getMessageId(), e));
+            } else {
+                log.log(Level.SEVERE, "Error invoking async call for:" + msg, e);
+            }
+        }   
+    }
+
+    /**
+     *  Called internally when an RpcResponseMessage is received from 
+     *  the remote connection.
+     */ 
+    public void handleMessage( RpcResponseMessage msg ) {
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "handleMessage({0})", msg);
+        }    
+        ResponseHolder holder = responses.remove(msg.getMessageId());
+        if( holder == null ) {
+            return;
+        }
+        holder.setResponse(msg);       
+    }
+ 
+    /**
+     *  Sort of like a Future, holds a locked reference to a response
+     *  until the remote call has completed and returned a response.
+     */   
+    private class ResponseHolder {
+        private Object response;
+        private String error;
+        private RpcCallMessage msg;
+        boolean received = false;
+ 
+        public ResponseHolder( RpcCallMessage msg ) {
+            this.msg = msg;
+        }
+        
+        public synchronized void setResponse( RpcResponseMessage msg ) {
+            this.response = msg.getResult();
+            this.error = msg.getError();
+            this.received = true;
+            notifyAll();
+        }
+        
+        public synchronized Object getResponse() {
+            try {
+                while(!received) {
+                    wait();                
+                }
+            } catch( InterruptedException e ) {
+                throw new RuntimeException("Interrupted waiting for respone to:" + msg, e);
+            }
+            if( error != null ) {
+                throw new RuntimeException("Error calling remote procedure:" + msg + "\n" + error);
+            }
+            return response;              
+        }
+        
+        public synchronized void release() {
+            if( received ) {
+                return;
+            }
+            // Else signal an error for the callers
+            this.error = "Closing connection";
+            this.received = true;
+        }
+    }
+}

+ 52 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcHandler.java

@@ -0,0 +1,52 @@
+/*
+ * 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.rpc;
+
+
+/**
+ *  Implementations of this interface can be registered with
+ *  the RpcClientService or RpcHostService to handle the 
+ *  remote procedure calls for a given object or objects.
+ *
+ *  @author    Paul Speed
+ */
+public interface RpcHandler {
+
+    /**
+     *  Called when a remote procedure call request is received for a particular
+     *  object from the other end of the network connection.
+     */
+    public Object call( RpcConnection conn, short objectId, short procId, Object... args );
+}
+
+

+ 227 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcHostedService.java

@@ -0,0 +1,227 @@
+/*
+ * 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.rpc;
+
+import com.jme3.network.HostedConnection;
+import com.jme3.network.Server;
+import com.jme3.network.serializing.Serializer;
+import com.jme3.network.util.SessionDataDelegator;
+import com.jme3.network.service.AbstractHostedService;
+import com.jme3.network.service.HostedServiceManager;
+import com.jme3.network.service.rpc.msg.RpcCallMessage;
+import com.jme3.network.service.rpc.msg.RpcResponseMessage;
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ *  RPC service that can be added to a network Server to
+ *  add RPC send/receive capabilities.  For a particular
+ *  HostedConnection, Remote procedure calls can be made to the 
+ *  associated Client and responses retrieved.  Any remote procedure 
+ *  calls that the Client performs for this connection will be 
+ *  received by this service and delegated to the appropriate RpcHandlers.
+ *
+ *  Note: it can be dangerous for a server to perform synchronous
+ *  RPC calls to a client but especially so if not done as part
+ *  of the response to some other message.  ie: iterating over all
+ *  or some HostedConnections to perform synchronous RPC calls
+ *  will be slow and potentially block the server's threads in ways
+ *  that can cause deadlocks or odd contention. 
+ *
+ *  @author    Paul Speed
+ */
+public class RpcHostedService extends AbstractHostedService {
+
+    private static final String ATTRIBUTE_NAME = "rpcSession";
+
+    static final Logger log = Logger.getLogger(RpcHostedService.class.getName());
+
+    private boolean autoHost;
+    private SessionDataDelegator delegator;
+
+    /**
+     *  Creates a new RPC host service that can be registered
+     *  with the Network server and will automatically 'host'
+     *  RPC services and each new network connection.
+     */
+    public RpcHostedService() {
+        this(true);
+    }
+    
+    /**
+     *  Creates a new RPC host service that can be registered
+     *  with the Network server and will optionally 'host'
+     *  RPC services and each new network connection depending
+     *  on the specified 'autoHost' flag.
+     */
+    public RpcHostedService( boolean autoHost ) {
+        this.autoHost = autoHost;
+        
+        // This works for me... has to be different in
+        // the general case
+        Serializer.registerClasses(RpcCallMessage.class, RpcResponseMessage.class);
+    }
+
+    /**
+     *  Used internally to setup the message delegator that will
+     *  handle HostedConnection specific messages and forward them
+     *  to that connection's RpcConnection.
+     */
+    @Override
+    protected void onInitialize( HostedServiceManager serviceManager ) {
+        Server server = serviceManager.getServer();
+         
+        // A general listener for forwarding the messages
+        // to the client-specific handler
+        this.delegator = new SessionDataDelegator(RpcConnection.class, 
+                                                  ATTRIBUTE_NAME,
+                                                  true);
+        server.addMessageListener(delegator, delegator.getMessageTypes());
+
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "Registered delegator for message types:{0}", Arrays.asList(delegator.getMessageTypes()));
+        }
+    }
+
+    /**
+     *  When set to true, all new connections will automatically have
+     *  RPC hosting services attached to them, meaning they can send
+     *  and receive RPC calls.  If this is set to false then it is up
+     *  to other services to eventually call startHostingOnConnection().
+     *  
+     *  <p>Reasons for doing this vary but usually would be because
+     *  the client shouldn't be allowed to perform any RPC calls until
+     *  it has provided more information.  In general, this is unnecessary
+     *  because the RpcHandler registries are not shared.  Each client
+     *  gets their own and RPC calls will fail until the appropriate
+     *  objects have been registtered.</p>
+     */
+    public void setAutoHost( boolean b ) {
+        this.autoHost = b;
+    }
+ 
+    /**
+     *  Returns true if this service automatically attaches RPC
+     *  hosting capabilities to new connections.
+     */   
+    public boolean getAutoHost() {
+        return autoHost;
+    }
+
+    /**
+     *  Retrieves the RpcConnection for the specified HostedConnection
+     *  if that HostedConnection has had RPC services started using
+     *  startHostingOnConnection() (or via autohosting).  Returns null
+     *  if the connection currently doesn't have RPC hosting services
+     *  attached.
+     */
+    public RpcConnection getRpcConnection( HostedConnection hc ) {
+        return hc.getAttribute(ATTRIBUTE_NAME);
+    }
+
+    /**
+     *  Sets up RPC hosting services for the hosted connection allowing
+     *  getRpcConnection() to return a valid RPC connection 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);
+        }
+        hc.setAttribute(ATTRIBUTE_NAME, new RpcConnection(hc));
+    }
+
+    /**
+     *  Removes any RPC hosting services associated with the specified
+     *  connection.  Calls to getRpcConnection() will return null for
+     *  this connection.  The connection's RpcConnection is also closed,
+     *  releasing any waiting synchronous calls with a "Connection closing"
+     *  error.
+     *  This method is called automatically for all leaving connections if
+     *  autohost is set to true.
+     */
+    public void stopHostingOnConnection( HostedConnection hc ) {
+        RpcConnection rpc = hc.getAttribute(ATTRIBUTE_NAME);
+        if( rpc == null ) {
+            return;
+        }
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "stopHostingOnConnection:{0}", hc);
+        }
+        hc.setAttribute(ATTRIBUTE_NAME, null);
+        rpc.close();
+    }
+
+    /**
+     *  Used internally to remove the message delegator from the
+     *  server.
+     */
+    @Override
+    public void terminate(HostedServiceManager serviceManager) {
+        Server server = serviceManager.getServer();
+        server.removeMessageListener(delegator, delegator.getMessageTypes());
+    }
+
+    /**
+     *  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});
+        }    
+        stopHostingOnConnection(hc);
+    }
+
+}
+

+ 98 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcCallMessage.java

@@ -0,0 +1,98 @@
+/*
+ * 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.rpc.msg;
+
+import com.jme3.network.AbstractMessage;
+import com.jme3.network.serializing.Serializable;
+
+ 
+/**
+ *  Used internally to send RPC call information to
+ *  the other end of a connection for execution.
+ *
+ *  @author    Paul Speed
+ */
+@Serializable
+public class RpcCallMessage extends AbstractMessage {
+
+    private long msgId;
+    private byte channel;
+    private short objId;
+    private short procId;
+    private Object[] args;
+
+    public RpcCallMessage() {
+    }
+    
+    public RpcCallMessage( long msgId, byte channel, short objId, short procId, Object... args ) {
+        this.msgId = msgId;
+        this.channel = channel;
+        this.objId = objId;
+        this.procId = procId;
+        this.args = args;
+    }
+ 
+    public long getMessageId() {
+        return msgId;
+    }
+    
+    public byte getChannel() {
+        return channel;
+    }
+ 
+    public boolean isAsync() {
+        return msgId == -1;
+    }
+ 
+    public short getObjectId() {
+        return objId;
+    }
+    
+    public short getProcedureId() {
+        return procId;
+    }
+    
+    public Object[] getArguments() {
+        return args;
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[#" + msgId + ", channel=" + channel
+                                          + (isAsync() ? ", async" : ", sync") 
+                                          + ", objId=" + objId 
+                                          + ", procId=" + procId 
+                                          + ", args.length=" + args.length 
+                                          + "]";
+    }
+}

+ 89 - 0
jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java

@@ -0,0 +1,89 @@
+/*
+ * 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.rpc.msg;
+
+import com.jme3.network.AbstractMessage;
+import com.jme3.network.serializing.Serializable;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+ 
+/**
+ *  Used internally to send an RPC call's response back to
+ *  the caller.
+ *
+ *  @author    Paul Speed
+ */
+@Serializable
+public class RpcResponseMessage extends AbstractMessage {
+
+    private long msgId;
+    private Object result;
+    private String error;
+
+    public RpcResponseMessage() {
+    }
+    
+    public RpcResponseMessage( long msgId, Object result ) {
+        this.msgId = msgId;
+        this.result = result;
+    }
+
+    public RpcResponseMessage( long msgId, Throwable t ) {
+        this.msgId = msgId;
+         
+        StringWriter sOut = new StringWriter();
+        PrintWriter out = new PrintWriter(sOut);
+        t.printStackTrace(out);
+        out.close();
+        this.error = sOut.toString();
+    }
+ 
+    public long getMessageId() {
+        return msgId;
+    }
+    
+    public Object getResult() {
+        return result;
+    }
+        
+    public String getError() {
+        return error;
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[#" + msgId + ", result=" + result
+                                          + "]";
+    }
+}

+ 69 - 0
jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java

@@ -0,0 +1,69 @@
+/*
+ * 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.serializer;
+
+import com.jme3.network.Client;
+import com.jme3.network.Message;
+import com.jme3.network.MessageListener;
+import com.jme3.network.message.SerializerRegistrationsMessage;
+import com.jme3.network.serializing.Serializer;
+import com.jme3.network.service.AbstractClientService;
+import com.jme3.network.service.ClientServiceManager;
+
+
+/**
+ *
+ *
+ *  @author    Paul Speed
+ */
+public class ClientSerializerRegistrationsService extends AbstractClientService 
+                                                  implements MessageListener<Client> {
+
+    @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);
+        
+        // Add our listener for that message type
+        serviceManager.getClient().addMessageListener(this, SerializerRegistrationsMessage.class); 
+    }
+
+    public void messageReceived( Client source, Message m ) {
+        // We only wait for one kind of message...
+        SerializerRegistrationsMessage msg = (SerializerRegistrationsMessage)m;
+        msg.registerAll();
+    }    
+}

+ 72 - 0
jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java

@@ -0,0 +1,72 @@
+/*
+ * 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.serializer;
+
+import com.jme3.network.HostedConnection;
+import com.jme3.network.Server;
+import com.jme3.network.message.SerializerRegistrationsMessage;
+import com.jme3.network.serializing.Serializer;
+import com.jme3.network.service.AbstractHostedService;
+import com.jme3.network.service.HostedServiceManager;
+
+
+/**
+ *
+ *
+ *  @author    Paul Speed
+ */
+public class ServerSerializerRegistrationsService extends AbstractHostedService {
+
+    @Override
+    protected void onInitialize( HostedServiceManager serviceManager ) {
+        // Make sure our message type is registered
+        Serializer.registerClass(SerializerRegistrationsMessage.class);
+        Serializer.registerClass(SerializerRegistrationsMessage.Registration.class);
+    }
+    
+    @Override
+    public void start() {
+        // Compile the registrations into a message we will
+        // send to all connecting clients
+        SerializerRegistrationsMessage.compile();
+    }
+    
+    @Override
+    public void connectionAdded(Server server, HostedConnection hc) {
+        // Just in case
+        super.connectionAdded(server, hc);
+        
+        // Send the client the registration information
+        hc.send(SerializerRegistrationsMessage.INSTANCE);
+    }
+}

+ 314 - 0
jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java

@@ -0,0 +1,314 @@
+/*
+ * 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.util;
+
+import com.jme3.network.Message;
+import com.jme3.network.MessageConnection;
+import com.jme3.network.MessageListener;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ *  A MessageListener implementation that will forward messages to methods
+ *  of a delegate object.  These methods can be automapped or manually
+ *  specified.  Subclasses provide specific implementations for how to
+ *  find the actual delegate object.
+ *
+ *  @author    Paul Speed
+ */
+public abstract class AbstractMessageDelegator<S extends MessageConnection> 
+                                implements MessageListener<S> {
+                                
+    static final Logger log = Logger.getLogger(AbstractMessageDelegator.class.getName());                                
+                                
+    private Class delegateType;
+    private Map<Class, Method> methods = new HashMap<Class, Method>();
+    private Class[] messageTypes;
+ 
+    /**
+     *  Creates an AbstractMessageDelegator that will forward received
+     *  messages to methods of the specified delegate type.  If automap
+     *  is true then reflection is used to lookup probably message handling
+     *  methods.
+     */   
+    protected AbstractMessageDelegator( Class delegateType, boolean automap ) {
+        this.delegateType = delegateType;
+        if( automap ) {
+            automap();
+        }
+    }
+ 
+    /**
+     *  Returns the array of messages known to be handled by this message
+     *  delegator.
+     */
+    public Class[] getMessageTypes() {
+        if( messageTypes == null ) {
+            messageTypes = methods.keySet().toArray(new Class[methods.size()]);
+        }
+        return messageTypes;
+    }
+ 
+    /**
+     *  Returns true if the specified method is valid for the specified
+     *  message type.  This is used internally during automapping to
+     *  provide implementation specific filting of methods.
+     *  This implementation checks for methods that take either the connection and message 
+     *  type arguments (in that order) or just the message type.
+     */
+    protected boolean isValidMethod( Method m, Class messageType ) {
+ 
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "isValidMethod({0}, {1})", new Object[]{m, messageType});
+        }
+               
+        // Parameters must be S and message type or just message type
+        Class<?>[] parms = m.getParameterTypes();
+        if( parms.length != 2 && parms.length != 1 ) {
+            log.finest("Parameter count is not 1 or 2");
+            return false;
+        }                   
+        int messageIndex = 0;
+        if( parms.length > 1 ) {
+            if( MessageConnection.class.isAssignableFrom(parms[0]) ) {
+                messageIndex++;
+            } else {
+                log.finest("First paramter is not a MessageConnection or subclass.");
+                return false;
+            }
+        }
+ 
+        if( messageType == null && !Message.class.isAssignableFrom(parms[messageIndex]) ) {
+            log.finest("Second paramter is not a Message or subclass.");
+            return false;
+        }
+        if( messageType != null && !parms[messageIndex].isAssignableFrom(messageType) ) {
+            log.log(Level.FINEST, "Second paramter is not a {0}", messageType);
+            return false;
+        }
+        return true;            
+    }
+ 
+    /**
+     *  Convenience method that returns the message type as
+     *  reflecively determined for a particular method.  This
+     *  only works with methods that actually have arguments.
+     *  This implementation returns the last element of the method's
+     *  getParameterTypes() array, thus supporting both 
+     *  method(connection, messageType) as well as just method(messageType)
+     *  calling forms.
+     */
+    protected Class getMessageType( Method m ) {
+        Class<?>[] parms = m.getParameterTypes();
+        return parms[parms.length-1];    
+    }
+ 
+    /**
+     *  Goes through all of the delegate type's methods to find
+     *  a method of the specified name that may take the specified 
+     *  message type.
+     */   
+    protected Method findDelegate( String name, Class messageType ) {
+        // We do an exhaustive search because it's easier to 
+        // check for a variety of parameter types and it's all
+        // that Class would be doing in getMethod() anyway.
+        for( Method m : delegateType.getDeclaredMethods() ) {
+                    
+            if( !m.getName().equals(name) ) {
+                continue;
+            }                
+ 
+            if( isValidMethod(m, messageType) ) {
+                return m;
+            }
+        }
+                       
+        return null;        
+    }
+    
+    /**
+     *  Returns true if the specified method name is allowed.
+     *  This is used by automapping to determine if a method
+     *  should be rejected purely on name.  Default implemention
+     *  always returns true.
+     */
+    protected boolean allowName( String name ) {
+        return true;
+    }
+ 
+    /**
+     *  Calls the map(Set) method with a null argument causing
+     *  all available matching methods to mapped to message types.
+     */    
+    protected final void automap() {        
+        map((Set<String>)null);
+        if( methods.isEmpty() ) {
+            throw new RuntimeException("No message handling methods found for class:" + delegateType);
+        }
+    }
+ 
+    /**
+     *  Specifically maps the specified methods names, autowiring
+     *  the parameters.
+     */
+    public AbstractMessageDelegator<S> map( String... methodNames ) {
+        Set<String> names = new HashSet<String>( Arrays.asList(methodNames) );
+        map(names);
+        return this;
+    }
+ 
+    /**
+     *  Goes through all of the delegate type's declared methods
+     *  mapping methods that match the current constraints.
+     *  If the constraints set is null then allowName() is
+     *  checked for names otherwise only names in the constraints
+     *  set are allowed.
+     *  For each candidate method that passes the above checks,
+     *  isValidMethod() is called with a null message type argument.
+     *  All methods are made accessible thus supporting non-public
+     *  methods as well as public methods.    
+     */   
+    protected void map( Set<String> constraints ) {
+        
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "map({0})", constraints);
+        } 
+        for( Method m : delegateType.getDeclaredMethods() ) {        
+            if( log.isLoggable(Level.FINEST) ) {
+                log.log(Level.FINEST, "Checking method:{0}", m);
+            }
+
+            if( constraints == null && !allowName(m.getName()) ) {
+                log.finest("Name is not allowed.");
+                continue;
+            }
+            if( constraints != null && !constraints.contains(m.getName()) ) {
+                log.finest("Name is not in constraints set.");
+                continue;
+            }
+
+            if( isValidMethod(m, null) ) {
+                if( log.isLoggable(Level.FINEST) ) {
+                    log.log(Level.FINEST, "Adding method mapping:{0} = {1}", new Object[]{getMessageType(m), m});
+                }
+                // Make sure we can access the method even if it's not public or
+                // is in a non-public inner class.
+                m.setAccessible(true);  
+                methods.put(getMessageType(m), m);
+            }            
+        }
+        
+        messageTypes = null;        
+    }
+ 
+    /**
+     *  Manually maps a specified method to the specified message type.
+     */   
+    public AbstractMessageDelegator<S> map( Class messageType, String methodName ) {
+        // Lookup the method 
+        Method m = findDelegate( methodName, messageType );
+        if( m == null ) { 
+            throw new RuntimeException( "Method:" + methodName 
+                                        + " not found matching signature (MessageConnection, " 
+                                        + messageType.getName() + ")" );
+        }                                        
+ 
+        if( log.isLoggable(Level.FINEST) ) {
+            log.log(Level.FINEST, "Adding method mapping:{0} = {1}", new Object[]{messageType, m});
+        }  
+        methods.put( messageType, m );
+        messageTypes = null;        
+        return this;   
+    }
+    
+    /**
+     *  Returns the mapped method for the specified message type.
+     */
+    protected Method getMethod( Class c ) {
+        Method m = methods.get(c);
+        return m;
+    }
+
+    /**
+     *  Implemented by subclasses to provide the actual delegate object
+     *  against which the mapped message type methods will be called.
+     */ 
+    protected abstract Object getSourceDelegate( S source );
+
+    /**
+     *  Implementation of the MessageListener's messageReceived()
+     *  method that will use the current message type mapping to
+     *  find an appropriate message handling method and call it
+     *  on the delegate returned by getSourceDelegate().
+     */
+    @Override
+    public void messageReceived( S source, Message msg ) {
+        if( msg == null ) {
+            return;
+        }
+ 
+        Object delegate = getSourceDelegate(source);
+        if( delegate == null ) {
+            // Means ignore this message/source
+            return;
+        } 
+            
+        Method m = getMethod(msg.getClass());
+        if( m == null ) {
+            throw new RuntimeException("Delegate method not found for message class:" 
+                                        + msg.getClass());
+        }
+ 
+        try {
+            if( m.getParameterTypes().length > 1 ) {           
+                m.invoke( delegate, source, msg );
+            } else {
+                m.invoke( delegate, msg );
+            }
+        } catch( IllegalAccessException e ) {
+            throw new RuntimeException("Error executing:" + m, e);
+        } catch( InvocationTargetException e ) {
+            throw new RuntimeException("Error executing:" + m, e.getCause());
+        }
+    }
+}
+
+

+ 72 - 0
jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java

@@ -0,0 +1,72 @@
+/*
+ * 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.util;
+
+import com.jme3.network.MessageConnection;
+
+
+/**
+ *  A MessageListener implementation that will forward messages to methods
+ *  of a specified delegate object.  These methods can be automapped or manually
+ *  specified.  
+ *
+ *  @author    Paul Speed
+ */
+public class ObjectMessageDelegator<S extends MessageConnection> extends AbstractMessageDelegator<S> {
+ 
+    private Object delegate;
+    
+    /**
+     *  Creates a MessageListener that will forward mapped message types
+     *  to methods of the specified object.
+     *  If automap is true then all methods with the proper signature will
+     *  be mapped.
+     *  <p>Methods of the following signatures are allowed:
+     *  <ul>
+     *  <li>void someName(S conn, SomeMessage msg)
+     *  <li>void someName(Message msg)
+     *  </ul>
+     *  Where S is the type of MessageConnection and SomeMessage is some
+     *  specific concreate Message subclass.
+     */   
+    public ObjectMessageDelegator( Object delegate, boolean automap ) {
+        super(delegate.getClass(), automap);
+        this.delegate = delegate;
+    }
+ 
+    @Override
+    protected Object getSourceDelegate( MessageConnection source ) {
+        return delegate;
+    }
+}
+

+ 103 - 0
jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2015 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.network.util;
+
+import com.jme3.network.HostedConnection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ *  A MessageListener implementation that will forward messages to methods
+ *  of a delegate specified as a HostedConnection session attribute.  This is
+ *  useful for handling connection-specific messages from clients that must
+ *  delegate to client-specific data objects.
+ *  The delegate methods can be automapped or manually specified.  
+ *
+ *  @author    Paul Speed
+ */
+public class SessionDataDelegator extends AbstractMessageDelegator<HostedConnection> {
+ 
+    static final Logger log = Logger.getLogger(SessionDataDelegator.class.getName());
+    
+    private String attributeName;
+ 
+    /**
+     *  Creates a MessageListener that will forward mapped message types
+     *  to methods of an object specified as a HostedConnection attribute.
+     *  If automap is true then all methods with the proper signature will
+     *  be mapped.
+     *  <p>Methods of the following signatures are allowed:
+     *  <ul>
+     *  <li>void someName(S conn, SomeMessage msg)
+     *  <li>void someName(Message msg)
+     *  </ul>
+     *  Where S is the type of MessageConnection and SomeMessage is some
+     *  specific concreate Message subclass.
+     */   
+    public SessionDataDelegator( Class delegateType, String attributeName, boolean automap ) {
+        super(delegateType, automap);
+        this.attributeName = attributeName;
+    }
+ 
+    /**
+     *  Returns the attribute name that will be used to look up the 
+     *  delegate object.
+     */   
+    public String getAttributeName() {
+        return attributeName;
+    }
+ 
+    /**
+     *  Called internally when there is no session object
+     *  for the current attribute name attached to the passed source
+     *  HostConnection.  Default implementation logs a warning.
+     */
+    protected void miss( HostedConnection source ) {
+        log.log(Level.WARNING, "Session data is null for:{0} on connection:{1}", new Object[]{attributeName, source});
+    }
+ 
+    /**
+     *  Returns the attributeName attribute of the supplied source
+     *  HostConnection.  If there is no value at that attribute then
+     *  the miss() method is called.
+     */   
+    protected Object getSourceDelegate( HostedConnection source ) {
+        Object result = source.getAttribute(attributeName);
+        if( result == null ) {
+            miss(source);
+        }
+        return result;
+    }
+}
+

+ 9 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/ContentTextureKey.java

@@ -32,6 +32,8 @@
 package com.jme3.scene.plugins.fbx;
 
 import com.jme3.asset.TextureKey;
+import com.jme3.asset.cache.AssetCache;
+import com.jme3.asset.cache.WeakRefCloneAssetCache;
 import com.jme3.export.InputCapsule;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
@@ -56,6 +58,13 @@ public class ContentTextureKey extends TextureKey {
 		return content;
 	}
 	
+        @Override
+        public Class<? extends AssetCache> getCacheType(){
+            // Need to override this so that textures embedded in FBX
+            // don't get cached by the asset manager.
+            return null;
+        }
+        
 	@Override
 	public String toString() {
 		return super.toString() + " " + content.length + " bytes";

+ 413 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/FbxLoader.java

@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx;
+
+import com.jme3.animation.AnimControl;
+import com.jme3.animation.Animation;
+import com.jme3.animation.Bone;
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.Skeleton;
+import com.jme3.animation.Track;
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.ModelKey;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Transform;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.plugins.fbx.anim.FbxToJmeTrack;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimCurveNode;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimLayer;
+import com.jme3.scene.plugins.fbx.anim.FbxAnimStack;
+import com.jme3.scene.plugins.fbx.anim.FbxBindPose;
+import com.jme3.scene.plugins.fbx.anim.FbxLimbNode;
+import com.jme3.scene.plugins.fbx.file.FbxDump;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxFile;
+import com.jme3.scene.plugins.fbx.file.FbxReader;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+import com.jme3.scene.plugins.fbx.misc.FbxGlobalSettings;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import com.jme3.scene.plugins.fbx.node.FbxRootNode;
+import com.jme3.scene.plugins.fbx.obj.FbxObjectFactory;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxLoader implements AssetLoader {
+    
+    private static final Logger logger = Logger.getLogger(FbxLoader.class.getName());
+    
+    private AssetManager assetManager;
+    
+    private String sceneName;
+    private String sceneFilename;
+    private String sceneFolderName;
+    private FbxGlobalSettings globalSettings;
+    private final Map<FbxId, FbxObject> objectMap = new HashMap<FbxId, FbxObject>();
+    
+    private final List<FbxAnimStack> animStacks = new ArrayList<FbxAnimStack>();
+    private final List<FbxBindPose> bindPoses = new ArrayList<FbxBindPose>();
+    
+    @Override
+    public Object load(AssetInfo assetInfo) throws IOException {
+        this.assetManager = assetInfo.getManager();
+        AssetKey<?> assetKey = assetInfo.getKey();
+        if (!(assetKey instanceof ModelKey)) {
+            throw new AssetLoadException("Invalid asset key");
+        }
+        
+        InputStream stream = assetInfo.openStream();
+        try {
+            sceneFilename = assetKey.getName();
+            sceneFolderName = assetKey.getFolder();
+            String ext = assetKey.getExtension();
+            
+            sceneName = sceneFilename.substring(0, sceneFilename.length() - ext.length() - 1);
+            if (sceneFolderName != null && sceneFolderName.length() > 0) {
+                sceneName = sceneName.substring(sceneFolderName.length());
+            }
+            
+            reset();
+            
+            // Load the data from the stream.
+            loadData(stream);
+            
+            // Bind poses are needed to compute world transforms.
+            applyBindPoses();
+            
+            // Need world transforms for skeleton creation.
+            updateWorldTransforms();
+            
+            // Need skeletons for meshs to be created in scene graph construction.
+            // Mesh bone indices require skeletons to determine bone index.
+            constructSkeletons();
+            
+            // Create the jME3 scene graph from the FBX scene graph.
+            // Also creates SkeletonControls based on the constructed skeletons.
+            Spatial scene = constructSceneGraph();
+            
+            // Load animations into AnimControls
+            constructAnimations();
+           
+            return scene;
+        } finally {
+            releaseObjects();
+            if (stream != null) {
+                stream.close();
+            }
+        }
+    }
+    
+    private void reset() {
+        globalSettings = new FbxGlobalSettings();
+    }
+    
+    private void releaseObjects() {
+        globalSettings = null;
+        objectMap.clear();
+        animStacks.clear();
+    }
+    
+    private void loadData(InputStream stream) throws IOException {
+        FbxFile scene = FbxReader.readFBX(stream);
+
+        FbxDump.dumpFile(scene);
+
+        // TODO: Load FBX object templates
+        
+        for (FbxElement e : scene.rootElements) {
+            if (e.id.equals("FBXHeaderExtension")) {
+                loadHeader(e);
+            } else if (e.id.equals("GlobalSettings")) {
+                loadGlobalSettings(e);
+             } else if (e.id.equals("Objects")) {
+                loadObjects(e);
+            } else if (e.id.equals("Connections")) {
+                connectObjects(e);
+            }
+        }
+    }
+    
+    private void loadHeader(FbxElement element) {
+        for (FbxElement e : element.children) {
+            if (e.id.equals("FBXVersion")) {
+                Integer version = (Integer) e.properties.get(0);
+                if (version < 7100) {
+                    logger.log(Level.WARNING, "FBX file version is older than 7.1. "
+                                            + "Some features may not work.");
+                }
+            }
+        }
+    }
+    
+    private void loadGlobalSettings(FbxElement element) {
+        globalSettings = new FbxGlobalSettings();
+        globalSettings.fromElement(element);
+    }
+    
+    private void loadObjects(FbxElement element) {
+        // Initialize the FBX root element.
+        objectMap.put(FbxId.ROOT, new FbxRootNode(assetManager, sceneFolderName));
+
+        for(FbxElement e : element.children) {
+            if (e.id.equals("GlobalSettings")) {
+                // Old FBX files seem to have the GlobalSettings element
+                // under Objects (??)
+                globalSettings.fromElement(e);
+            } else {
+                FbxObject object = FbxObjectFactory.createObject(e, assetManager, sceneFolderName);
+                if (object != null) {
+                    if (objectMap.containsKey(object.getId())) {
+                        logger.log(Level.WARNING, "An object with ID \"{0}\" has "
+                                                + "already been defined. "
+                                                + "Ignoring.", 
+                                                   object.getId());
+                    }
+                    
+                    objectMap.put(object.getId(), object);
+                    
+                    if (object instanceof FbxAnimStack) {
+                        // NOTE: animation stacks are implicitly global.
+                        // Capture them here.
+                        animStacks.add((FbxAnimStack) object);
+                    } else if (object instanceof FbxBindPose) {
+                        bindPoses.add((FbxBindPose) object);
+                    }
+                } else {
+                    throw new UnsupportedOperationException("Failed to create FBX object of type: " + e.id);
+                }
+            }
+        }
+    }
+    
+    private void removeUnconnectedObjects() {
+        for (FbxObject object : new ArrayList<FbxObject>(objectMap.values())) {
+            if (!object.isJmeObjectCreated()) {
+                logger.log(Level.WARNING, "Purging orphan FBX object: {0}", object);
+                objectMap.remove(object.getId());
+            }
+        }
+    }
+    
+    private void connectObjects(FbxElement element) {
+        if (objectMap.isEmpty()) {
+            logger.log(Level.WARNING, "FBX file is missing object information");
+            return;
+        } else if (objectMap.size() == 1) {
+            // Only root node (automatically added by jME3)
+            logger.log(Level.WARNING, "FBX file has no objects");
+            return;
+        }
+        
+        for (FbxElement el : element.children) {
+            if (!el.id.equals("C") && !el.id.equals("Connect")) {
+                continue;
+            }
+            String type = (String) el.properties.get(0);
+            FbxId childId;
+            FbxId parentId;
+            if (type.equals("OO")) {
+                childId = FbxId.create(el.properties.get(1));
+                parentId = FbxId.create(el.properties.get(2));
+                FbxObject child = objectMap.get(childId);
+                FbxObject parent;
+
+                if (parentId.isNull()) {
+                    // TODO: maybe clean this up a bit..
+                    parent = objectMap.get(FbxId.ROOT);
+                } else {
+                    parent = objectMap.get(parentId);
+                }
+
+                if (parent == null) {
+                    throw new UnsupportedOperationException("Cannot find parent object ID \"" + parentId + "\"");
+                }
+
+                parent.connectObject(child);
+            } else if (type.equals("OP")) {
+                childId = FbxId.create(el.properties.get(1));
+                parentId = FbxId.create(el.properties.get(2));
+                String propName = (String) el.properties.get(3);
+                FbxObject child = objectMap.get(childId);
+                FbxObject parent = objectMap.get(parentId);
+                parent.connectObjectProperty(child, propName);
+            } else {
+                logger.log(Level.WARNING, "Unknown connection type: {0}. Ignoring.", type);
+            }
+        }
+    }
+    
+    /**
+     * Copies the bind poses from FBX BindPose objects to FBX nodes.
+     * Must be called prior to {@link #updateWorldTransforms()}.
+     */
+    private void applyBindPoses() {
+        for (FbxBindPose bindPose : bindPoses) {
+            Map<FbxId, Matrix4f> bindPoseData = bindPose.getJmeObject();
+            logger.log(Level.INFO, "Applying {0} bind poses", bindPoseData.size());
+            for (Map.Entry<FbxId, Matrix4f> entry : bindPoseData.entrySet()) {
+                FbxObject obj = objectMap.get(entry.getKey());
+                if (obj instanceof FbxNode) {
+                    FbxNode node = (FbxNode) obj;
+                    node.setWorldBindPose(entry.getValue());
+                } else {
+                    logger.log(Level.WARNING, "Bind pose can only be applied to FBX nodes. Ignoring.");
+                }
+            }
+        }
+    }
+    
+    /**
+     * Updates world transforms and bind poses for the FBX scene graph.
+     */
+    private void updateWorldTransforms() {
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+        fbxRoot.updateWorldTransforms(null, null);
+    }
+    
+    private void constructAnimations() {
+        // In FBX, animation are not attached to any root.
+        // They are implicitly global.
+        // So, we need to use hueristics to find which node(s) 
+        // an animation is associated with, so we can create the AnimControl
+        // in the appropriate location in the scene.
+        Map<FbxToJmeTrack, FbxToJmeTrack> pairs = new HashMap<FbxToJmeTrack, FbxToJmeTrack>();
+        for (FbxAnimStack stack : animStacks) {
+            for (FbxAnimLayer layer : stack.getLayers()) {
+                for (FbxAnimCurveNode curveNode : layer.getAnimationCurveNodes()) {
+                    for (Map.Entry<FbxNode, String> nodePropertyEntry : curveNode.getInfluencedNodeProperties().entrySet()) {
+                        FbxToJmeTrack lookupPair = new FbxToJmeTrack();
+                        lookupPair.animStack = stack;
+                        lookupPair.animLayer = layer;
+                        lookupPair.node = nodePropertyEntry.getKey();
+                        
+                        // Find if this pair is already stored
+                        FbxToJmeTrack storedPair = pairs.get(lookupPair);
+                        if (storedPair == null) {
+                            // If not, store it.
+                            storedPair = lookupPair;
+                            pairs.put(storedPair, storedPair);
+                        }
+                        
+                        String property = nodePropertyEntry.getValue();
+                        storedPair.animCurves.put(property, curveNode);
+                    }
+                }
+            }
+        }
+        
+        // At this point we can construct the animation for all pairs ...
+        for (FbxToJmeTrack pair : pairs.values()) {
+            String animName = pair.animStack.getName();
+            float duration    = pair.animStack.getDuration();
+            
+            System.out.println("ANIMATION: " + animName + ", duration = " + duration);
+            System.out.println("NODE: " + pair.node.getName());
+            
+            duration = pair.getDuration();
+            
+            if (pair.node instanceof FbxLimbNode) {
+                // Find the spatial that has the skeleton for this limb.
+                FbxLimbNode limbNode = (FbxLimbNode) pair.node;
+                Bone bone = limbNode.getJmeBone();
+                Spatial jmeSpatial = limbNode.getSkeletonHolder().getJmeObject();
+                Skeleton skeleton = limbNode.getSkeletonHolder().getJmeSkeleton();
+                
+                // Get the animation control (create if missing).
+                AnimControl animControl = jmeSpatial.getControl(AnimControl.class);
+                if (animControl.getSkeleton() != skeleton) {
+                    throw new UnsupportedOperationException();
+                }
+                
+                // Get the animation (create if missing).
+                Animation anim = animControl.getAnim(animName);
+                if (anim == null) { 
+                    anim = new Animation(animName, duration);
+                    animControl.addAnim(anim);
+                }
+                
+                // Find the bone index from the spatial's skeleton.
+                int boneIndex = skeleton.getBoneIndex(bone);
+                
+                // Generate the bone track.
+                BoneTrack bt = pair.toJmeBoneTrack(boneIndex, bone.getBindInverseTransform());
+                
+                // Add the bone track to the animation.
+                anim.addTrack(bt);
+            } else {
+                // Create the spatial animation
+                Animation anim = new Animation(animName, duration);
+                anim.setTracks(new Track[]{ pair.toJmeSpatialTrack() });
+
+                // Get the animation control (create if missing).
+                Spatial jmeSpatial = pair.node.getJmeObject();
+                AnimControl animControl = jmeSpatial.getControl(AnimControl.class);
+                
+                if (animControl == null) { 
+                    animControl = new AnimControl(null);
+                    jmeSpatial.addControl(animControl);
+                }
+                
+                // Add the spatial animation
+                animControl.addAnim(anim);
+            }
+        }
+    }
+    
+    private void constructSkeletons() {
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+        FbxNode.createSkeletons(fbxRoot);
+    }
+    
+    private Spatial constructSceneGraph() {
+        // Acquire the implicit root object.
+        FbxNode fbxRoot = (FbxNode) objectMap.get(FbxId.ROOT);
+
+        // Convert it into a jME3 scene
+        Node jmeRoot = (Node) FbxNode.createScene(fbxRoot);
+
+        // Fix the name (will probably be set to something like "-node")
+        jmeRoot.setName(sceneName + "-scene");
+
+        return jmeRoot;
+    }
+}

+ 42 - 42
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/SceneLoader.java

@@ -73,9 +73,9 @@ import com.jme3.scene.Mesh.Mode;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Usage;
 import com.jme3.scene.plugins.fbx.AnimationList.AnimInverval;
-import com.jme3.scene.plugins.fbx.file.FBXElement;
-import com.jme3.scene.plugins.fbx.file.FBXFile;
-import com.jme3.scene.plugins.fbx.file.FBXReader;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxFile;
+import com.jme3.scene.plugins.fbx.file.FbxReader;
 import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture2D;
@@ -165,8 +165,8 @@ public class SceneLoader implements AssetLoader {
 	private void loadScene(InputStream stream) throws IOException {
 		logger.log(Level.FINE, "Loading scene {0}", sceneFilename);
 		long startTime = System.currentTimeMillis();
-		FBXFile scene = FBXReader.readFBX(stream);
-		for(FBXElement e : scene.rootElements) {
+		FbxFile scene = FbxReader.readFBX(stream);
+		for(FbxElement e : scene.rootElements) {
 			if(e.id.equals("GlobalSettings"))
 				loadGlobalSettings(e);
 			else if(e.id.equals("Objects"))
@@ -178,10 +178,10 @@ public class SceneLoader implements AssetLoader {
 		logger.log(Level.FINE, "Loading done in {0} ms", estimatedTime);
 	}
 	
-	private void loadGlobalSettings(FBXElement element) {
-		for(FBXElement e : element.children) {
+	private void loadGlobalSettings(FbxElement element) {
+		for(FbxElement e : element.children) {
 			if(e.id.equals("Properties70")) {
-				for(FBXElement e2 : e.children) {
+				for(FbxElement e2 : e.children) {
 					if(e2.id.equals("P")) {
 						String propName = (String) e2.properties.get(0);
 						if(propName.equals("UnitScaleFactor"))
@@ -197,8 +197,8 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadObjects(FBXElement element) {
-		for(FBXElement e : element.children) {
+	private void loadObjects(FbxElement element) {
+		for(FbxElement e : element.children) {
 			if(e.id.equals("Geometry"))
 				loadGeometry(e);
 			else if(e.id.equals("Material"))
@@ -222,12 +222,12 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadGeometry(FBXElement element) {
+	private void loadGeometry(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String type = (String) element.properties.get(2);
 		if(type.equals("Mesh")) {
 			MeshData data = new MeshData();
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("Vertices"))
 					data.vertices = (double[]) e.properties.get(0);
 				else if(e.id.equals("PolygonVertexIndex"))
@@ -236,7 +236,7 @@ public class SceneLoader implements AssetLoader {
 				//else if(e.id.equals("Edges"))
 				//	data.edges = (int[]) e.properties.get(0);
 				else if(e.id.equals("LayerElementNormal"))
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("MappingInformationType")) {
 							data.normalsMapping = (String) e2.properties.get(0);
 							if(!data.normalsMapping.equals("ByVertice") && !data.normalsMapping.equals("ByPolygonVertex"))
@@ -249,7 +249,7 @@ public class SceneLoader implements AssetLoader {
 							data.normals = (double[]) e2.properties.get(0);
 					}
 				else if(e.id.equals("LayerElementTangent"))
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("MappingInformationType")) {
 							data.tangentsMapping = (String) e2.properties.get(0);
 							if(!data.tangentsMapping.equals("ByVertice") && !data.tangentsMapping.equals("ByPolygonVertex"))
@@ -262,7 +262,7 @@ public class SceneLoader implements AssetLoader {
 							data.tangents = (double[]) e2.properties.get(0);
 					}
 				else if(e.id.equals("LayerElementBinormal"))
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("MappingInformationType")) {
 							data.binormalsMapping = (String) e2.properties.get(0);
 							if(!data.binormalsMapping.equals("ByVertice") && !data.binormalsMapping.equals("ByPolygonVertex"))
@@ -275,7 +275,7 @@ public class SceneLoader implements AssetLoader {
 							data.binormals = (double[]) e2.properties.get(0);
 					}
 				else if(e.id.equals("LayerElementUV"))
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("MappingInformationType")) {
 							data.uvMapping = (String) e2.properties.get(0);
 							if(!data.uvMapping.equals("ByPolygonVertex"))
@@ -291,7 +291,7 @@ public class SceneLoader implements AssetLoader {
 					}
 				// TODO smoothing is not used now
 				//else if(e.id.equals("LayerElementSmoothing"))
-				//	for(FBXElement e2 : e.children) {
+				//	for(FbxElement e2 : e.children) {
 				//		if(e2.id.equals("MappingInformationType")) {
 				//			data.smoothingMapping = (String) e2.properties.get(0);
 				//			if(!data.smoothingMapping.equals("ByEdge"))
@@ -304,7 +304,7 @@ public class SceneLoader implements AssetLoader {
 				//			data.smoothing = (int[]) e2.properties.get(0);
 				//	}
 				else if(e.id.equals("LayerElementMaterial"))
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("MappingInformationType")) {
 							data.materialsMapping = (String) e2.properties.get(0);
 							if(!data.materialsMapping.equals("AllSame"))
@@ -321,18 +321,18 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadMaterial(FBXElement element) {
+	private void loadMaterial(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		if(type.equals("")) {
 			MaterialData data = new MaterialData();
 			data.name = path.substring(0, path.indexOf(0));
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("ShadingModel")) {
 					data.shadingModel = (String) e.properties.get(0);
 				} else if(e.id.equals("Properties70")) {
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("P")) {
 							String propName = (String) e2.properties.get(0);
 							if(propName.equals("AmbientColor")) {
@@ -368,16 +368,16 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadModel(FBXElement element) {
+	private void loadModel(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		ModelData data = new ModelData();
 		data.name = path.substring(0, path.indexOf(0));
 		data.type = type;
-		for(FBXElement e : element.children) {
+		for(FbxElement e : element.children) {
 			if(e.id.equals("Properties70")) {
-				for(FBXElement e2 : e.children) {
+				for(FbxElement e2 : e.children) {
 					if(e2.id.equals("P")) {
 						String propName = (String) e2.properties.get(0);
 						if(propName.equals("Lcl Translation")) {
@@ -408,17 +408,17 @@ public class SceneLoader implements AssetLoader {
 		modelDataMap.put(id, data);
 	}
 	
-	private void loadPose(FBXElement element) {
+	private void loadPose(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		if(type.equals("BindPose")) {
 			BindPoseData data = new BindPoseData();
 			data.name = path.substring(0, path.indexOf(0));
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("PoseNode")) {
 					NodeTransformData item = new NodeTransformData();
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("Node"))
 							item.nodeId = (Long) e2.properties.get(0);
 						else if(e2.id.equals("Matrix"))
@@ -431,14 +431,14 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadTexture(FBXElement element) {
+	private void loadTexture(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		if(type.equals("")) {
 			TextureData data = new TextureData();
 			data.name = path.substring(0, path.indexOf(0));
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("Type"))
 					data.bindType = (String) e.properties.get(0);
 				else if(e.id.equals("FileName"))
@@ -448,14 +448,14 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadImage(FBXElement element) {
+	private void loadImage(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		if(type.equals("Clip")) {
 			ImageData data = new ImageData();
 			data.name = path.substring(0, path.indexOf(0));
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("Type"))
 					data.type = (String) e.properties.get(0);
 				else if(e.id.equals("FileName"))
@@ -471,19 +471,19 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadDeformer(FBXElement element) {
+	private void loadDeformer(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String type = (String) element.properties.get(2);
 		if(type.equals("Skin")) {
 			SkinData skinData = new SkinData();
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("SkinningType"))
 					skinData.type = (String) e.properties.get(0);
 			}
 			skinMap.put(id, skinData);
 		} else if(type.equals("Cluster")) {
 			ClusterData clusterData = new ClusterData();
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("Indexes"))
 					clusterData.indexes = (int[]) e.properties.get(0);
 				else if(e.id.equals("Weights"))
@@ -497,7 +497,7 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadAnimLayer(FBXElement element) {
+	private void loadAnimLayer(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
@@ -508,12 +508,12 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadAnimCurve(FBXElement element) {
+	private void loadAnimCurve(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String type = (String) element.properties.get(2);
 		if(type.equals("")) {
 			AnimCurveData data = new AnimCurveData();
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("KeyTime"))
 					data.keyTimes = (long[]) e.properties.get(0);
 				else if(e.id.equals("KeyValueFloat"))
@@ -523,15 +523,15 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadAnimNode(FBXElement element) {
+	private void loadAnimNode(FbxElement element) {
 		long id = (Long) element.properties.get(0);
 		String path = (String) element.properties.get(1);
 		String type = (String) element.properties.get(2);
 		if(type.equals("")) {
 			Double x = null, y = null, z = null;
-			for(FBXElement e : element.children) {
+			for(FbxElement e : element.children) {
 				if(e.id.equals("Properties70")) {
-					for(FBXElement e2 : e.children) {
+					for(FbxElement e2 : e.children) {
 						if(e2.id.equals("P")) {
 							String propName = (String) e2.properties.get(0);
 							if(propName.equals("d|X"))
@@ -554,8 +554,8 @@ public class SceneLoader implements AssetLoader {
 		}
 	}
 	
-	private void loadConnections(FBXElement element) {
-		for(FBXElement e : element.children) {
+	private void loadConnections(FbxElement element) {
+		for(FbxElement e : element.children) {
 			if(e.id.equals("C")) {
 				String type = (String) e.properties.get(0);
 				long objId, refId;

+ 144 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurve.java

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.FastMath;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+
+public class FbxAnimCurve extends FbxObject {
+
+    private long[] keyTimes;
+    private float[] keyValues;
+    
+    public FbxAnimCurve(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        
+        for (FbxElement e : element.children) {
+            if (e.id.equals("KeyTime")) {
+                keyTimes = (long[]) e.properties.get(0);
+            } else if (e.id.equals("KeyValueFloat")) {
+                keyValues = (float[]) e.properties.get(0);
+            }
+        }
+        
+        long time = -1;
+        for (int i = 0; i < keyTimes.length; i++) {
+            if (time >= keyTimes[i]) {
+                throw new UnsupportedOperationException("Keyframe times must be sequential, but they are not.");
+            }
+            time = keyTimes[i];
+        }
+    }
+
+    /**
+     * Get the times for the keyframes.
+     * @return Keyframe times. 
+     */
+    public long[] getKeyTimes() {
+        return keyTimes;
+    }
+    
+    /**
+     * Retrieve the curve value at the given time.
+     * If the curve has no data, 0 is returned.
+     * If the time is outside the curve, then the closest value is returned.
+     * If the time isn't on an exact keyframe, linear interpolation is used
+     * to determine the value between the keyframes at the given time.
+     * @param time The time to get the curve value at (in FBX time units).
+     * @return The value at the given time.
+     */
+    public float getValueAtTime(long time) {
+        if (keyTimes.length == 0) {
+            return 0;
+        }
+        
+        // If the time is outside the range, 
+        // we just return the closest value. (No extrapolation)
+        if (time <= keyTimes[0]) {
+            return keyValues[0];
+        } else if (time >= keyTimes[keyTimes.length - 1]) {
+            return keyValues[keyValues.length - 1];
+        }
+        
+        
+
+        int startFrame = 0;
+        int endFrame = 1;
+        int lastFrame = keyTimes.length - 1;
+        
+        for (int i = 0; i < lastFrame && keyTimes[i] < time; ++i) {
+            startFrame = i;
+            endFrame = i + 1;
+        }
+        
+        long keyTime1    = keyTimes[startFrame];
+        float keyValue1  = keyValues[startFrame];
+        long keyTime2    = keyTimes[endFrame];
+        float keyValue2  = keyValues[endFrame];
+        
+        if (keyTime2 == time) {
+            return keyValue2;
+        }
+        
+        long prevToNextDelta    = keyTime2 - keyTime1;
+        long prevToCurrentDelta = time     - keyTime1;
+        float lerpAmount = (float)prevToCurrentDelta / prevToNextDelta;
+        
+        return FastMath.interpolateLinear(lerpAmount, keyValue1, keyValue2);
+    }
+
+    @Override
+    protected Object toJmeObject() {
+        // An AnimCurve has no jME3 representation.
+        // The parent AnimCurveNode is responsible to create the jME3 
+        // representation.
+        throw new UnsupportedOperationException("No jME3 object conversion available");
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 147 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimCurveNode.java

@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxAnimCurveNode extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimCurveNode.class.getName());
+    
+    private final Map<FbxNode, String> influencedNodePropertiesMap = new HashMap<FbxNode, String>();
+    private final Map<String, FbxAnimCurve> propertyToCurveMap = new HashMap<String, FbxAnimCurve>();
+    private final Map<String, Float> propertyToDefaultMap = new HashMap<String, Float>();
+    
+    public FbxAnimCurveNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement prop : element.getFbxProperties()) {
+            String propName = (String) prop.properties.get(0);
+            String propType = (String) prop.properties.get(1);
+            if (propType.equals("Number")) {
+                float propValue = ((Double) prop.properties.get(4)).floatValue();
+                propertyToDefaultMap.put(propName, propValue);
+            }
+        }
+    }
+    
+    public void addInfluencedNode(FbxNode node, String property) {
+        influencedNodePropertiesMap.put(node, property);
+    }
+    
+    public Map<FbxNode, String> getInfluencedNodeProperties() {
+        return influencedNodePropertiesMap;
+    }
+    
+    public Collection<FbxAnimCurve> getCurves() {
+        return propertyToCurveMap.values();
+    }
+    
+    public Vector3f getVector3Value(long time) {
+        Vector3f value = new Vector3f();
+        FbxAnimCurve xCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_X);
+        FbxAnimCurve yCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Y);
+        FbxAnimCurve zCurve = propertyToCurveMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Z);
+        Float xDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_X);
+        Float yDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Y);
+        Float zDefault      = propertyToDefaultMap.get(FbxAnimUtil.CURVE_NODE_PROPERTY_Z);
+        value.x = xCurve != null ? xCurve.getValueAtTime(time) : xDefault;
+        value.y = yCurve != null ? yCurve.getValueAtTime(time) : yDefault;
+        value.z = zCurve != null ? zCurve.getValueAtTime(time) : zDefault;
+        return value;
+    }
+    
+    /**
+     * Converts the euler angles from {@link #getVector3Value(long)} to
+     * a quaternion rotation.
+     * @param time Time at which to get the euler angles.
+     * @return The rotation at time
+     */
+    public Quaternion getQuaternionValue(long time) {
+        Vector3f eulerAngles = getVector3Value(time);
+        System.out.println("\tT: " + time + ". Rotation: " + 
+                                eulerAngles.x + ", " + 
+                                eulerAngles.y + ", " + eulerAngles.z);
+        Quaternion q = new Quaternion();
+        q.fromAngles(eulerAngles.x * FastMath.DEG_TO_RAD, 
+                     eulerAngles.y * FastMath.DEG_TO_RAD, 
+                     eulerAngles.z * FastMath.DEG_TO_RAD);
+        return q;
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        if (!(object instanceof FbxAnimCurve)) {
+            unsupportedConnectObjectProperty(object, property);
+        }
+        if (!property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_X) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_Y) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_Z) &&
+            !property.equals(FbxAnimUtil.CURVE_NODE_PROPERTY_VISIBILITY)) {
+            logger.log(Level.WARNING, "Animating the dimension ''{0}'' is not "
+                                    + "supported yet. Ignoring.", property);
+            return;
+        }
+        
+        if (propertyToCurveMap.containsKey(property)) {
+            throw new UnsupportedOperationException("!");
+        }
+        
+        propertyToCurveMap.put(property, (FbxAnimCurve) object);
+    }
+    
+}

+ 82 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimLayer.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class FbxAnimLayer extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimLayer.class.getName());
+    
+    private final List<FbxAnimCurveNode> animCurves = new ArrayList<FbxAnimCurveNode>();
+    
+    public FbxAnimLayer(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        // No known properties for layers.. 
+        // Also jME3 doesn't support multiple layers anyway.
+    }
+    
+    public List<FbxAnimCurveNode> getAnimationCurveNodes() {
+        return Collections.unmodifiableList(animCurves);
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (!(object instanceof FbxAnimCurveNode)) {
+            unsupportedConnectObject(object);
+        }
+        
+        animCurves.add((FbxAnimCurveNode) object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}
+    

+ 111 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimStack.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxAnimStack extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxAnimStack.class.getName());
+    
+    private float duration;
+    private FbxAnimLayer layer0;
+    
+    public FbxAnimStack(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement child : element.getFbxProperties()) {
+            String propName = (String) child.properties.get(0);
+            if (propName.equals("LocalStop")) {
+                long durationLong = (Long)child.properties.get(4);
+                duration = (float) (durationLong * FbxAnimUtil.SECONDS_PER_UNIT);
+            }
+        }
+    }
+    
+//    /**
+//     * Finds out which FBX nodes this animation is going to influence.
+//     * 
+//     * @return A list of FBX nodes that the stack's curves are influencing.
+//     */
+//    public Set<FbxNode> getInfluencedNodes() {
+//        HashSet<FbxNode> influencedNodes = new HashSet<FbxNode>();
+//        if (layer0 == null) {
+//            return influencedNodes;
+//        }
+//        for (FbxAnimCurveNode curveNode : layer0.getAnimationCurveNodes()) {
+//            influencedNodes.addAll(curveNode.getInfluencedNodes());
+//        }
+//        return influencedNodes;
+//    }
+
+    public float getDuration() {
+        return duration;
+    }
+
+    public FbxAnimLayer[] getLayers() {
+        return new FbxAnimLayer[]{ layer0 };
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+    
+    @Override
+    public void connectObject(FbxObject object) {
+        if (!(object instanceof FbxAnimLayer)) {
+            unsupportedConnectObject(object);
+        }
+        
+        if (layer0 != null) {
+            logger.log(Level.WARNING, "jME3 does not support layered animation. "
+                                    + "Only first layer has been loaded.");
+            return;
+        }
+        
+        layer0 = (FbxAnimLayer) object;
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}

+ 44 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxAnimUtil.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+public class FbxAnimUtil {
+    /**
+     * Conversion factor from FBX animation time unit to seconds.
+     */
+    public static final double SECONDS_PER_UNIT = 1 / 46186158000d;
+    
+    public static final String CURVE_NODE_PROPERTY_X          = "d|X";
+    public static final String CURVE_NODE_PROPERTY_Y          = "d|Y";
+    public static final String CURVE_NODE_PROPERTY_Z          = "d|Z";
+    public static final String CURVE_NODE_PROPERTY_VISIBILITY = "d|Visibility";
+}

+ 103 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxBindPose.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.math.Matrix4f;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.file.FbxId;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FbxBindPose extends FbxObject<Map<FbxId, Matrix4f>> {
+
+    private final Map<FbxId, Matrix4f> bindPose = new HashMap<FbxId, Matrix4f>();
+    
+    public FbxBindPose(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement child : element.children) {
+            if (!child.id.equals("PoseNode")) {
+                continue;
+            }
+            
+            FbxId node = null;
+            float[] matData = null;
+            
+            for (FbxElement e : child.children) {
+                if (e.id.equals("Node")) {
+                    node = FbxId.create(e.properties.get(0));
+                } else if (e.id.equals("Matrix")) {
+                    double[] matDataDoubles = (double[]) e.properties.get(0);
+                    
+                    if (matDataDoubles.length != 16) {
+                        // corrupt
+                        throw new UnsupportedOperationException("Bind pose matrix "
+                                + "must have 16 doubles, but it has " 
+                                + matDataDoubles.length + ". Data is corrupt");
+                    }
+                    
+                    matData = new float[16];
+                    for (int i = 0; i < matDataDoubles.length; i++) {
+                        matData[i] = (float) matDataDoubles[i];
+                    }
+                }
+            }
+            
+            if (node != null && matData != null) {
+                Matrix4f matrix = new Matrix4f(matData);
+                bindPose.put(node, matrix);
+            }
+        }
+    }
+    
+    @Override
+    protected Map<FbxId, Matrix4f> toJmeObject() {
+        return bindPose;
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        unsupportedConnectObject(object);
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 98 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxCluster.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.file.FbxElement;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class FbxCluster extends FbxObject {
+
+    private static final Logger logger = Logger.getLogger(FbxCluster.class.getName());
+    
+    private int[] indexes;
+    private double[] weights;
+    private FbxLimbNode limb;
+    
+    public FbxCluster(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    public void fromElement(FbxElement element) {
+        super.fromElement(element);
+        for (FbxElement e : element.children) {
+            if (e.id.equals("Indexes")) {
+                indexes = (int[]) e.properties.get(0);
+            } else if (e.id.equals("Weights")) {
+                weights = (double[]) e.properties.get(0);
+            }
+        }
+    }
+
+    public int[] getVertexIndices() {
+        return indexes;
+    }
+
+    public double[] getWeights() {
+        return weights;
+    }
+
+    public FbxLimbNode getLimb() {
+        return limb;
+    }
+    
+    @Override
+    protected Object toJmeObject() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxLimbNode) {
+            if (limb != null) {
+                logger.log(Level.WARNING, "This cluster already has a limb attached. Ignoring.");
+                return;
+            }
+            limb = (FbxLimbNode) object;
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+}

+ 94 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxLimbNode.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.animation.Bone;
+import com.jme3.animation.Skeleton;
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FbxLimbNode extends FbxNode {
+    
+    protected FbxNode skeletonHolder;
+    protected Bone bone;
+    
+    public FbxLimbNode(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    private static void createBones(FbxNode skeletonHolderNode, FbxLimbNode limb, List<Bone> bones) {
+        limb.skeletonHolder = skeletonHolderNode;
+        
+        Bone parentBone = limb.getJmeBone();
+        bones.add(parentBone);
+        
+        for (FbxNode child : limb.children) {
+            if (child instanceof FbxLimbNode) {
+                FbxLimbNode childLimb = (FbxLimbNode) child;
+                createBones(skeletonHolderNode, childLimb, bones);
+                parentBone.addChild(childLimb.getJmeBone());
+            }
+        }
+    }
+    
+    public static Skeleton createSkeleton(FbxNode skeletonHolderNode) {
+        if (skeletonHolderNode instanceof FbxLimbNode) {
+            throw new UnsupportedOperationException("Limb nodes cannot be skeleton holders");
+        }
+        
+        List<Bone> bones = new ArrayList<Bone>();
+        
+        for (FbxNode child : skeletonHolderNode.getChildren()) {
+            if (child instanceof FbxLimbNode) {
+                createBones(skeletonHolderNode, (FbxLimbNode) child, bones);
+            }
+        }
+        
+        return new Skeleton(bones.toArray(new Bone[0]));
+    }
+    
+    public FbxNode getSkeletonHolder() {
+        return skeletonHolder;
+    }
+    
+    public Bone getJmeBone() {
+        if (bone == null) {
+            bone = new Bone(name);
+            bone.setBindTransforms(jmeLocalBindPose.getTranslation(),
+                                   jmeLocalBindPose.getRotation(),
+                                   jmeLocalBindPose.getScale());
+        }
+        return bone;
+    }
+}

+ 66 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxSkinDeformer.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.scene.plugins.fbx.obj.FbxObject;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FbxSkinDeformer extends FbxObject<List<FbxCluster>> {
+
+    private final List<FbxCluster> clusters = new ArrayList<FbxCluster>();
+    
+    public FbxSkinDeformer(AssetManager assetManager, String sceneFolderName) {
+        super(assetManager, sceneFolderName);
+    }
+    
+    @Override
+    protected List<FbxCluster> toJmeObject() {
+        return clusters;
+    }
+
+    @Override
+    public void connectObject(FbxObject object) {
+        if (object instanceof FbxCluster) {
+            clusters.add((FbxCluster) object);
+        } else {
+            unsupportedConnectObject(object);
+        }
+    }
+
+    @Override
+    public void connectObjectProperty(FbxObject object, String property) {
+        unsupportedConnectObjectProperty(object, property);
+    }
+    
+}

+ 202 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/anim/FbxToJmeTrack.java

@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2009-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.scene.plugins.fbx.anim;
+
+import com.jme3.animation.BoneTrack;
+import com.jme3.animation.SpatialTrack;
+import com.jme3.animation.Track;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.plugins.fbx.node.FbxNode;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Maps animation stacks to influenced nodes. 
+ * Will be used later to create jME3 tracks.
+ */
+public final class FbxToJmeTrack {
+
+    public FbxAnimStack animStack;
+    public FbxAnimLayer animLayer;
+    public FbxNode node;
+
+    // These are not used in map lookups.
+    public transient final Map<String, FbxAnimCurveNode> animCurves = new HashMap<String, FbxAnimCurveNode>();
+
+    public long[] getKeyTimes() {
+        Set<Long> keyFrameTimesSet = new HashSet<Long>();
+        for (FbxAnimCurveNode curveNode : animCurves.values()) {
+            for (FbxAnimCurve curve : curveNode.getCurves()) {
+                for (long keyTime : curve.getKeyTimes()) {
+                    keyFrameTimesSet.add(keyTime);
+                }
+            }
+        }
+        long[] keyFrameTimes = new long[keyFrameTimesSet.size()];
+        int i = 0;
+        for (Long keyFrameTime : keyFrameTimesSet) {
+            keyFrameTimes[i++] = keyFrameTime;
+        }
+        Arrays.sort(keyFrameTimes);
+        return keyFrameTimes;
+    }
+    
+    /**
+     * Generate a {@link BoneTrack} from the animation data, for the given
+     * boneIndex.
+     * 
+     * @param boneIndex The bone index for which track data is generated for.
+     * @param inverseBindPose Inverse bind pose of the bone (in world space).
+     * @return A BoneTrack containing the animation data, for the specified
+     * boneIndex.
+     */
+    public BoneTrack toJmeBoneTrack(int boneIndex, Transform inverseBindPose) {
+        return (BoneTrack) toJmeTrackInternal(boneIndex, inverseBindPose);
+    }
+    
+    public SpatialTrack toJmeSpatialTrack() {
+        return (SpatialTrack) toJmeTrackInternal(-1, null);
+    }
+    
+    public float getDuration() {
+        long[] keyframes = getKeyTimes();
+        return (float) (keyframes[keyframes.length - 1] * FbxAnimUtil.SECONDS_PER_UNIT);
+    }
+    
+    private static void applyInverse(Vector3f translation, Quaternion rotation, Vector3f scale, Transform inverseBindPose) {
+        Transform t = new Transform();
+        t.setTranslation(translation);
+        t.setRotation(rotation);
+        if (scale != null) {
+            t.setScale(scale);
+        }
+        t.combineWithParent(inverseBindPose);
+        
+        t.getTranslation(translation);
+        t.getRotation(rotation);
+        if (scale != null) {
+            t.getScale(scale);
+        }
+    }
+    
+    private Track toJmeTrackInternal(int boneIndex, Transform inverseBindPose) {
+        float duration = animStack.getDuration();
+        
+        FbxAnimCurveNode translationCurve = animCurves.get("Lcl Translation");
+        FbxAnimCurveNode rotationCurve    = animCurves.get("Lcl Rotation");
+        FbxAnimCurveNode scalingCurve     = animCurves.get("Lcl Scaling");
+
+        long[] fbxTimes = getKeyTimes();
+        float[] times = new float[fbxTimes.length];
+        
+        // Translations / Rotations must be set on all tracks.
+        // (Required for jME3)
+        Vector3f[]   translations = new Vector3f[fbxTimes.length];
+        Quaternion[] rotations = new Quaternion[fbxTimes.length];
+        
+        Vector3f[] scales = null;
+        if (scalingCurve != null) {
+            scales = new Vector3f[fbxTimes.length];
+        }
+         
+         for (int i = 0; i < fbxTimes.length; i++) {
+            long fbxTime = fbxTimes[i];
+            float time = (float) (fbxTime * FbxAnimUtil.SECONDS_PER_UNIT);
+
+            if (time > duration) {
+                // Expand animation duration to fit the curve.
+                duration = time;
+                System.out.println("actual duration: " + duration);
+            }
+
+            times[i] = time;
+            if (translationCurve != null) {
+                translations[i] = translationCurve.getVector3Value(fbxTime);
+            } else {
+                translations[i] = new Vector3f();
+            }
+            if (rotationCurve != null) {
+                rotations[i] = rotationCurve.getQuaternionValue(fbxTime);
+                if (i > 0) {
+                    if (rotations[i - 1].dot(rotations[i]) < 0) {
+                        System.out.println("rotation will go the long way, oh noes");
+                        rotations[i - 1].negate();
+                    }
+                }
+            } else {
+                rotations[i] = new Quaternion();
+            }
+            if (scalingCurve != null) {
+                scales[i] = scalingCurve.getVector3Value(fbxTime);
+            }
+            
+            if (inverseBindPose != null) {
+                applyInverse(translations[i], rotations[i], scales != null ? scales[i] : null, inverseBindPose);
+            }
+        }
+        
+        if (boneIndex == -1) {
+            return new SpatialTrack(times, translations, rotations, scales);
+        } else {
+            if (scales != null) {
+                return new BoneTrack(boneIndex, times, translations, rotations, scales);
+            } else {
+                return new BoneTrack(boneIndex, times, translations, rotations);
+            }
+        }
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 79 * hash + this.animStack.hashCode();
+        hash = 79 * hash + this.animLayer.hashCode();
+        hash = 79 * hash + this.node.hashCode();
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        final FbxToJmeTrack other = (FbxToJmeTrack) obj;
+        return this.node == other.node
+                && this.animStack == other.animStack
+                && this.animLayer == other.animLayer;
+    }
+}

+ 43 - 33
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FBXDump.java → jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxDump.java

@@ -37,7 +37,8 @@ import java.lang.reflect.Array;
 import java.text.DecimalFormat;
 import java.util.HashMap;
 import java.util.Map;
-import static org.omg.IOP.IORHelper.id;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * Quick n' dirty dumper of FBX binary files.
@@ -46,12 +47,14 @@ import static org.omg.IOP.IORHelper.id;
  * 
  * @author Kirill Vainer
  */
-public final class FBXDump {
+public final class FbxDump {
+    
+    private static final Logger logger = Logger.getLogger(FbxDump.class.getName());
     
     private static final DecimalFormat DECIMAL_FORMAT 
             = new DecimalFormat("0.0000000000");
     
-    private FBXDump() { }
+    private FbxDump() { }
     
     /**
      * Creates a map between object UIDs and the objects themselves.
@@ -59,16 +62,17 @@ public final class FBXDump {
      * @param file The file to create the mappings for.
      * @return The UID to object map.
      */
-    private static Map<Long, FBXElement> createUidToObjectMap(FBXFile file) {
-        Map<Long, FBXElement> uidToObjectMap = new HashMap<Long, FBXElement>();
-        for (FBXElement rootElement : file.rootElements) {
+    private static Map<FbxId, FbxElement> createUidToObjectMap(FbxFile file) {
+        Map<FbxId, FbxElement> uidToObjectMap = new HashMap<FbxId, FbxElement>();
+        for (FbxElement rootElement : file.rootElements) {
             if (rootElement.id.equals("Objects")) {
-                for (FBXElement fbxObj : rootElement.children) {
-                    if (fbxObj.propertiesTypes[0] != 'L') {
-                        continue; // error
+                for (FbxElement fbxObj : rootElement.children) {
+                    FbxId uid = FbxId.getObjectId(fbxObj);
+                    if (uid != null) {
+                        uidToObjectMap.put(uid, fbxObj);
+                    } else {
+                        logger.log(Level.WARNING, "Cannot determine ID for object: {0}", fbxObj);
                     }
-                    Long uid = (Long) fbxObj.properties.get(0);
-                    uidToObjectMap.put(uid, fbxObj);
                 }
             }
         }
@@ -80,8 +84,8 @@ public final class FBXDump {
      * 
      * @param file the file to dump.
      */
-    public static void dumpFBX(FBXFile file) {
-        dumpFBX(file, System.out);
+    public static void dumpFile(FbxFile file) {
+        dumpFile(file, System.out);
     }
     
     /**
@@ -90,11 +94,11 @@ public final class FBXDump {
      * @param file the file to dump.
      * @param out the output stream where to output.
      */
-    public static void dumpFBX(FBXFile file, OutputStream out) {
-        Map<Long, FBXElement> uidToObjectMap = createUidToObjectMap(file);
+    public static void dumpFile(FbxFile file, OutputStream out) {
+        Map<FbxId, FbxElement> uidToObjectMap = createUidToObjectMap(file);
         PrintStream ps = new PrintStream(out);
-        for (FBXElement rootElement : file.rootElements) {
-            dumpFBXElement(rootElement, ps, 0, uidToObjectMap);
+        for (FbxElement rootElement : file.rootElements) {
+            dumpElement(rootElement, ps, 0, uidToObjectMap);
         }
     }
     
@@ -113,9 +117,9 @@ public final class FBXDump {
         return string.replaceAll("\u0000\u0001", "::");
     }
 
-    protected static void dumpFBXProperty(String id, char propertyType, 
+    protected static void dumpProperty(String id, char propertyType, 
                                           Object property, PrintStream ps, 
-                                          Map<Long, FBXElement> uidToObjectMap) {
+                                          Map<FbxId, FbxElement> uidToObjectMap) {
         switch (propertyType) {
             case 'S':
                 // String
@@ -125,13 +129,19 @@ public final class FBXDump {
             case 'R':
                 // RAW data.
                 byte[] bytes = (byte[]) property;
-                ps.print("[");
-                for (int j = 0; j < bytes.length; j++) {
+                int numToPrint = Math.min(10 * 1024, bytes.length);
+                ps.print("(size = ");
+                ps.print(bytes.length);
+                ps.print(") [");
+                for (int j = 0; j < numToPrint; j++) {
                     ps.print(String.format("%02X", bytes[j] & 0xff));
                     if (j != bytes.length - 1) {
                         ps.print(" ");
                     }
                 }
+                if (numToPrint < bytes.length) {
+                    ps.print(" ...");
+                }
                 ps.print("]");
                 break;
             case 'D':
@@ -159,7 +169,7 @@ public final class FBXDump {
                 // If this is a connection, decode UID into object name.
                 if (id.equals("C")) {
                     Long uid = (Long) property;
-                    FBXElement element = uidToObjectMap.get(uid);
+                    FbxElement element = uidToObjectMap.get(FbxId.create(uid));
                     if (element != null) {
                         String name = (String) element.properties.get(1);
                         ps.print("\"" + convertFBXString(name) + "\"");
@@ -178,7 +188,7 @@ public final class FBXDump {
                 int length = Array.getLength(property);
                 for (int j = 0; j < length; j++) {
                     Object arrayEntry = Array.get(property, j);
-                    dumpFBXProperty(id, Character.toUpperCase(propertyType), arrayEntry, ps, uidToObjectMap);
+                    dumpProperty(id, Character.toUpperCase(propertyType), arrayEntry, ps, uidToObjectMap);
                     if (j != length - 1) {
                         ps.print(",");
                     }
@@ -189,24 +199,24 @@ public final class FBXDump {
         }
     }
     
-    protected static void dumpFBXElement(FBXElement el, PrintStream ps, 
-                                         int indent, Map<Long, FBXElement> uidToObjectMap) {
+    protected static void dumpElement(FbxElement el, PrintStream ps, 
+                                         int indent, Map<FbxId, FbxElement> uidToObjectMap) {
         // 4 spaces per tab should be OK.
         String indentStr = indent(indent * 4);
         String textId = el.id;
         
         // Properties are called 'P' and connections are called 'C'.
-        if (el.id.equals("P")) {
-            textId = "Property";
-        } else if (el.id.equals("C")) {
-            textId = "Connect";
-        }
+//        if (el.id.equals("P")) {
+//            textId = "Property";
+//        } else if (el.id.equals("C")) {
+//            textId = "Connect";
+//        }
         
         ps.print(indentStr + textId + ": ");
         for (int i = 0; i < el.properties.size(); i++) {
             Object property = el.properties.get(i);
             char propertyType = el.propertiesTypes[i];
-            dumpFBXProperty(el.id, propertyType, property, ps, uidToObjectMap);
+            dumpProperty(el.id, propertyType, property, ps, uidToObjectMap);
             if (i != el.properties.size() - 1) {
                 ps.print(", ");
             }
@@ -215,8 +225,8 @@ public final class FBXDump {
             ps.println();
         } else {
             ps.println(" {");
-            for (FBXElement childElement : el.children) {
-                dumpFBXElement(childElement, ps, indent + 1, uidToObjectMap);
+            for (FbxElement childElement : el.children) {
+                dumpElement(childElement, ps, indent + 1, uidToObjectMap);
             }
             ps.println(indentStr + "}");
         }

+ 124 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/file/FbxElement.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2009-2014 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.scene.plugins.fbx.file;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FbxElement {
+	
+	public String id;
+	public List<Object> properties;
+	/*
+	 * Y - signed short
+	 * C - boolean
+	 * I - signed integer
+	 * F - float
+	 * D - double
+	 * L - long
+	 * R - byte array
+	 * S - string
+	 * f - array of floats
+	 * i - array of ints
+	 * d - array of doubles
+	 * l - array of longs
+	 * b - array of boleans
+	 * c - array of unsigned bytes (represented as array of ints)
+	 */
+	public char[] propertiesTypes;
+	public List<FbxElement> children = new ArrayList<FbxElement>();
+	
+	public FbxElement(int propsCount) {
+		this.properties = new ArrayList<Object>(propsCount);
+		this.propertiesTypes = new char[propsCount];
+	}
+        
+        public FbxElement getChildById(String name) {
+            for (FbxElement element : children) {
+                if (element.id.equals(name)) {
+                    return element;
+                }
+            }
+            return null;
+        }
+        
+        public List<FbxElement> getFbxProperties() {
+            List<FbxElement> props = new ArrayList<FbxElement>();
+            FbxElement propsElement = null;
+            boolean legacy = false;
+            
+            for (FbxElement element : children) {
+                if (element.id.equals("Properties70")) {
+                    propsElement = element;
+                    break;
+                } else if (element.id.equals("Properties60")) {
+                    legacy = true;
+                    propsElement = element;
+                    break;
+                }
+            }
+            
+            if (propsElement == null) { 
+                return props;
+            }
+            
+            for (FbxElement prop : propsElement.children) {
+                if (prop.id.equals("P") || prop.id.equals("Property")) {
+                    if (legacy) {
+                        char[] types = new char[prop.propertiesTypes.length + 1];
+                        types[0] = prop.propertiesTypes[0];
+                        types[1] = prop.propertiesTypes[0];
+                        System.arraycopy(prop.propertiesTypes, 1, types, 2, types.length - 2);
+                        
+                        List<Object> values = new ArrayList<Object>(prop.properties);
+                        values.add(1, values.get(0));
+                        
+                        FbxElement dummyProp = new FbxElement(types.length);
+                        dummyProp.children = prop.children;
+                        dummyProp.id = prop.id;
+                        dummyProp.propertiesTypes = types;
+                        dummyProp.properties = values;
+                        props.add(dummyProp);
+                    } else {
+                        props.add(prop);
+                    }
+                }
+            }
+            
+            return props;
+        }
+        
+        @Override
+        public String toString() {
+            return "FBXElement[id=" + id + ", numProps=" + properties.size() + ", numChildren=" + children.size() + "]";
+        }
+}

部分文件因为文件数量过多而无法显示