Browse Source

Merge branch 'jMonkeyEngine:master' into master

joliver82 1 year ago
parent
commit
2800a9280a
100 changed files with 5472 additions and 1207 deletions
  1. 0 57
      .github/actions/tools/bintray.sh
  2. 22 0
      .github/actions/tools/minio.sh
  3. 1 30
      .github/actions/tools/uploadToMaven.sh
  4. 21 0
      .github/workflows/format.yml
  5. 95 54
      .github/workflows/main.yml
  6. 3 1
      .gitignore
  7. 4 4
      .vscode/JME_style.xml
  8. 6 0
      .vscode/extensions.json
  9. 12 2
      .vscode/settings.json
  10. 138 6
      CONTRIBUTING.md
  11. 0 29
      LICENSE
  12. 30 0
      LICENSE.md
  13. 30 16
      README.md
  14. 0 29
      bintray.gradle
  15. 45 76
      build.gradle
  16. 59 45
      common.gradle
  17. 6 0
      config/checkstyle/checkstyle-suppressions.xml
  18. 361 0
      config/checkstyle/checkstyle.xml
  19. 1 5
      gradle.properties
  20. 50 0
      gradle/libs.versions.toml
  21. BIN
      gradle/wrapper/gradle-wrapper.jar
  22. 3 1
      gradle/wrapper/gradle-wrapper.properties
  23. 172 111
      gradlew
  24. 25 33
      gradlew.bat
  25. 1 0
      jme-angle/src/native/angle
  26. 15 21
      jme3-android-examples/build.gradle
  27. 3 3
      jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java
  28. 7 7
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/CustomArrayAdapter.java
  29. 4 4
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java
  30. 62 0
      jme3-android-native/bufferallocator.gradle
  31. 3 17
      jme3-android-native/build.gradle
  32. 4 1
      jme3-android-native/decode.gradle
  33. 4 1
      jme3-android-native/openalsoft.gradle
  34. 50 0
      jme3-android-native/src/native/jme_bufferallocator/Android.mk
  35. 39 0
      jme3-android-native/src/native/jme_bufferallocator/Application.mk
  36. 99 0
      jme3-android-native/src/native/jme_bufferallocator/com_jme3_util_AndroidNativeBufferAllocator.c
  37. 25 20
      jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c
  38. 4 6
      jme3-android/build.gradle
  39. 12 6
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  40. 28 18
      jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java
  41. 8 6
      jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java
  42. 15 11
      jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
  43. 2 1
      jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java
  44. 35 0
      jme3-android/src/main/java/com/jme3/audio/android/package-info.java
  45. 93 9
      jme3-android/src/main/java/com/jme3/audio/plugins/NativeVorbisFile.java
  46. 37 6
      jme3-android/src/main/java/com/jme3/audio/plugins/NativeVorbisLoader.java
  47. 4 4
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java
  48. 2 2
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  49. 2 2
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java
  50. 9 7
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java
  51. 2 2
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java
  52. 31 23
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java
  53. 7 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java
  54. 33 21
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  55. 13 12
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  56. 2 2
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java
  57. 35 0
      jme3-android/src/main/java/com/jme3/input/android/package-info.java
  58. 41 11
      jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
  59. 9 1
      jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java
  60. 35 0
      jme3-android/src/main/java/com/jme3/renderer/android/package-info.java
  61. 42 34
      jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java
  62. 22 24
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  63. 84 8
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  64. 18 13
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidBufferImageLoader.java
  65. 32 9
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java
  66. 5 3
      jme3-android/src/main/java/com/jme3/util/AndroidBufferAllocator.java
  67. 3 0
      jme3-android/src/main/java/com/jme3/util/AndroidLogHandler.java
  68. 74 0
      jme3-android/src/main/java/com/jme3/util/AndroidNativeBufferAllocator.java
  69. 6 0
      jme3-android/src/main/java/com/jme3/util/AndroidScreenshots.java
  70. 36 0
      jme3-android/src/main/java/com/jme3/view/package-info.java
  71. 1014 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java
  72. 47 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/OnExceptionThrown.java
  73. 51 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/OnLayoutDrawn.java
  74. 53 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/OnRendererCompleted.java
  75. 55 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/OnRendererStarted.java
  76. 44 0
      jme3-android/src/main/java/com/jme3/view/surfaceview/package-info.java
  77. 2 1
      jme3-android/src/main/resources/com/jme3/asset/Android.cfg
  78. 3 0
      jme3-awt-dialogs/build.gradle
  79. 98 0
      jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTErrorDialog.java
  80. 234 179
      jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
  81. 48 0
      jme3-awt-dialogs/src/main/java/com/jme3/system/JmeDialogsFactoryImpl.java
  82. 6 6
      jme3-core/build.gradle
  83. 2 2
      jme3-core/src/main/java/checkers/quals/DefaultLocation.java
  84. 2 0
      jme3-core/src/main/java/checkers/quals/DefaultQualifier.java
  85. 3 3
      jme3-core/src/main/java/checkers/quals/DefaultQualifiers.java
  86. 2 2
      jme3-core/src/main/java/checkers/quals/Dependent.java
  87. 1 1
      jme3-core/src/main/java/checkers/quals/SubtypeOf.java
  88. 2 0
      jme3-core/src/main/java/checkers/quals/Unused.java
  89. 65 3
      jme3-core/src/main/java/com/jme3/anim/AnimClip.java
  90. 195 130
      jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
  91. 421 0
      jme3-core/src/main/java/com/jme3/anim/AnimFactory.java
  92. 330 0
      jme3-core/src/main/java/com/jme3/anim/AnimLayer.java
  93. 17 2
      jme3-core/src/main/java/com/jme3/anim/AnimTrack.java
  94. 7 1
      jme3-core/src/main/java/com/jme3/anim/AnimationMask.java
  95. 36 16
      jme3-core/src/main/java/com/jme3/anim/Armature.java
  96. 169 4
      jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
  97. 148 8
      jme3-core/src/main/java/com/jme3/anim/Joint.java
  98. 7 2
      jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
  99. 123 20
      jme3-core/src/main/java/com/jme3/anim/MorphControl.java
  100. 106 12
      jme3-core/src/main/java/com/jme3/anim/MorphTrack.java

+ 0 - 57
.github/actions/tools/bintray.sh

@@ -1,57 +0,0 @@
-#!/bin/bash
-
-# bintray_createPackage [REPO] [PACKAGE] [USER] [PASSWORD] [GIT REPO] [LICENSE]
-function bintray_createPackage {
-    repo="$1"
-    package="$2"
-    user="$3"
-    password="$4"
-    srcrepo="$5"
-    license="$6"
-
-    repoUrl="https://api.bintray.com/packages/$repo"
-    if [ "`curl -u$user:$password -H Content-Type:application/json -H Accept:application/json \
-    --write-out %{http_code} --silent --output /dev/null -X GET \"$repoUrl/$package\"`" != "200" ];
-    then
-
-        if [ "$srcrepo" != "" -a "$license" != "" ];
-        then 
-            echo "Package does not exist... create."
-            data="{
-                \"name\": \"${package}\",
-                \"labels\": [],
-                \"licenses\": [\"${license}\"],
-                \"vcs_url\": \"${srcrepo}\"
-            }"
-     
-
-            curl -u$user:$password -H "Content-Type:application/json" -H "Accept:application/json" -X POST \
-                -d "${data}" "$repoUrl"
-        else
-            echo "Package does not exist... you need to specify a repo and license for it to be created."
-        fi
-    else    
-        echo "The package already exists. Skip."
-    fi
-}
-
-# minio_uploadFile <LOCAL_FILEPATH> <REMOTE_FILEPATH> <MINIO_URL> <MINIO_ACCESS_KEY> <MINIO_SECRET_KEY>
-#
-# Upload the specified file to the specified MinIO instance.
-function minio_uploadFile {
-    file="$1"
-    dest="$2"
-    url="$3"
-    access="$4"
-    secret="$5"
-
-    echo "Install MinIO client"
-    wget --quiet https://dl.min.io/client/mc/release/linux-amd64/mc
-    chmod +x ./mc
-
-    echo "Add an alias for the MinIO instance to the MinIO configuration file"
-    ./mc alias set objects "$url" "$access" "$secret"
-
-    echo "Upload $file to $url/$dest"
-    ./mc cp "$file" "objects/$dest"
-}

+ 22 - 0
.github/actions/tools/minio.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# minio_uploadFile <LOCAL_FILEPATH> <REMOTE_FILEPATH> <MINIO_URL> <MINIO_ACCESS_KEY> <MINIO_SECRET_KEY>
+#
+# Upload the specified file to the specified MinIO instance.
+function minio_uploadFile {
+    file="$1"
+    dest="$2"
+    url="$3"
+    access="$4"
+    secret="$5"
+
+    echo "Install MinIO client"
+    wget --quiet https://dl.min.io/client/mc/release/linux-amd64/mc
+    chmod +x ./mc
+
+    echo "Add an alias for the MinIO instance to the MinIO configuration file"
+    ./mc alias set objects "$url" "$access" "$secret"
+
+    echo "Upload $file to $url/$dest"
+    ./mc cp "$file" "objects/$dest"
+}

+ 1 - 30
.github/actions/tools/uploadToMaven.sh

@@ -2,16 +2,10 @@
 #############################################
 #
 # Usage
-#       uploadAllToMaven path/of/dist/maven https://api.bintray.com/maven/riccardo/sandbox-maven/ riccardo $BINTRAY_PASSWORD gitrepo license
-#           Note: gitrepo and license are needed only when uploading to bintray if you want to create missing packages automatically
-#                   gitrepo must be a valid source repository
-#                   license must be a license supported by bintray eg "BSD 3-Clause"
-#   or
-#       uploadAllToMaven path/of/dist/maven $GITHUB_PACKAGE_REPOSITORY user password
+#   uploadAllToMaven path/of/dist/maven $GITHUB_PACKAGE_REPOSITORY user password
 #
 #############################################
 root="`dirname  ${BASH_SOURCE[0]}`"
-source $root/bintray.sh
 
 set -e
 function uploadToMaven {
@@ -34,29 +28,6 @@ function uploadToMaven {
         auth="-H \"Authorization: token $password\""
     fi
 
-    
-    if [[ $repourl == https\:\/\/api.bintray.com\/* ]]; 
-    then 
-        package="`dirname $destfile`"
-        version="`basename $package`"
-        package="`dirname $package`"
-        package="`basename $package`"
-
-        if [ "$user" = "" -o "$password" = "" ];
-        then
-            echo "Error! You need username and password to upload to bintray"
-             exit 1
-        fi
-        echo "Detected bintray"
-
-        bintrayRepo="${repourl/https\:\/\/api.bintray.com\/maven/}"   
-        echo "Create package on $bintrayRepo"
-
-        bintray_createPackage $bintrayRepo $package $user $password $srcrepo $license  
-        
-        repourl="$repourl/$package"    
-    fi
-
     cmd="curl -T \"$file\" $auth \
         \"$repourl/$destfile\" \
         -vvv"

+ 21 - 0
.github/workflows/format.yml

@@ -0,0 +1,21 @@
+name: auto-format
+on:
+  push:
+
+jobs:
+  format:
+    runs-on: ubuntu-latest
+    if: ${{ false }} 
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0   
+      - name: Prettify code
+        uses: creyD/[email protected]
+        with:
+          prettier_options: --tab-width 4 --print-width 110 --write **/**/*.java
+          prettier_version: "2.8.8"
+          only_changed: True
+          commit_message:  "auto-format"
+          prettier_plugins: "prettier-plugin-java"

+ 95 - 54
.github/workflows/main.yml

@@ -5,7 +5,7 @@
 #   - Build natives for android
 #   - Merge the natives, build the engine, create the zip release, maven artifacts, javadoc and native snapshot
 #   - (only when native code changes) Deploy the natives snapshot to the MinIO instance
-#   - (only when building a release) Deploy everything else to github releases, github packet registry, Bintray, and Sonatype
+#   - (only when building a release) Deploy everything else to github releases and Sonatype
 #   - (only when building a release) Update javadoc.jmonkeyengine.org
 # Note:
 #   All the actions/upload-artifact and actions/download-artifact steps are used to pass
@@ -13,13 +13,6 @@
 #   running workflow, we use it to store the result of each job since the filesystem
 #   is not maintained between jobs.
 ################# CONFIGURATIONS #####################################################
-# >> Configure BINTRAY RELEASE
-#   Configure the following secrets/variables (customize the values with your own)
-#     BINTRAY_GENERIC_REPO=riccardoblsandbox/jmonkeyengine-files
-#     BINTRAY_MAVEN_REPO=riccardoblsandbox/jmonkeyengine
-#     BINTRAY_USER=riccardo
-#     BINTRAY_APIKEY=XXXXXX
-#     BINTRAY_LICENSE="BSD 3-Clause"
 # >> Configure MINIO NATIVES SNAPSHOT
 #     OBJECTS_KEY=XXXXXX
 # >> Configure SONATYPE RELEASE
@@ -33,7 +26,7 @@
 #   is running the build.
 # >> Configure  JAVADOC
 #     JAVADOC_GHPAGES_REPO="riccardoblsandbox/javadoc.jmonkeyengine.org.git"
-#   Generate a deloy key
+#   Generate a deploy key
 #       ssh-keygen -t rsa -b 4096 -C "[email protected]" -f javadoc_deploy
 #   Set
 #     JAVADOC_GHPAGES_DEPLOY_PRIVKEY="......."
@@ -52,12 +45,12 @@ name: Build jMonkeyEngine
 on:
   push:
     branches:
-      - gsw
       - master
-      - newbuild
-      - v3.3.*
-      - v3.2
-      - v3.2.*
+      - v3.7
+      - v3.6
+      - v3.5
+      - v3.4
+      - v3.3
   pull_request:
   release:
     types: [published]
@@ -67,17 +60,17 @@ jobs:
   # Build the natives on android
   BuildAndroidNatives:
     name: Build natives for android
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-latest
     container:
       image: jmonkeyengine/buildenv-jme3:android
 
     steps:
       - name: Clone the repo
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 1
       - name: Validate the Gradle wrapper
-        uses: gradle/wrapper-validation-action@v1
+        uses: gradle/actions/wrapper-validation@v3
       - name: Build
         run: |
           ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
@@ -89,7 +82,7 @@ jobs:
           name: android-natives
           path: build/native
 
-  # Build the engine, we only deploy from ubuntu-18.04 jdk8
+  # Build the engine, we only deploy from ubuntu-latest jdk21
   BuildJMonkey:
     needs: [BuildAndroidNatives]
     name: Build on ${{ matrix.osName }} jdk${{ matrix.jdk }}
@@ -97,32 +90,34 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-18.04,ubuntu-20.04,windows-2019,macOS-latest]
-        jdk: [8.x.x,11.x.x]
+        os: [ubuntu-latest,windows-latest,macOS-latest]
+        jdk: [11, 17, 21]
         include:
-          - os: ubuntu-20.04
-            osName: linux-next
-          - os: ubuntu-18.04
+          - os: ubuntu-latest
             osName: linux
             deploy: true
-          - os: windows-2019
+          - os: windows-latest
             osName: windows
+            deploy: false
           - os: macOS-latest
             osName: mac
-          - jdk: 11.x.x
+            deploy: false
+          - jdk: 11
+            deploy: false
+          - jdk: 17
             deploy: false
 
     steps:
       - name: Clone the repo
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 1
 
       - name: Setup the java environment
-        uses: actions/setup-java@v1
+        uses: actions/setup-java@v4
         with:
+          distribution: 'temurin'
           java-version: ${{ matrix.jdk }}
-          architecture: x64
 
       - name: Download natives for android
         uses: actions/download-artifact@master
@@ -131,12 +126,12 @@ jobs:
           path: build/native
 
       - name: Validate the Gradle wrapper
-        uses: gradle/wrapper-validation-action@v1
+        uses: gradle/actions/wrapper-validation@v3
       - name: Build Engine
         shell: bash
         run: |
           # Build
-          ./gradlew -i -PuseCommitHashAsVersionName=true -PskipPrebuildLibraries=true build
+          ./gradlew -PuseCommitHashAsVersionName=true -PskipPrebuildLibraries=true build
 
           if [ "${{ matrix.deploy }}" = "true" ];
           then
@@ -212,10 +207,10 @@ jobs:
   # The snapshot is downloaded when people build the engine without setting buildNativeProject
   # this is useful for people that want to build only the java part and don't have
   # all the stuff needed to compile natives.
-  DeploySnapshot:
+  DeployNativeSnapshot:
     needs: [BuildJMonkey]
-    name: "Deploy snapshot"
-    runs-on: ubuntu-18.04
+    name: "Deploy native snapshot"
+    runs-on: ubuntu-latest
     if: github.event_name == 'push'
     steps:
 
@@ -236,7 +231,7 @@ jobs:
 
       - name: Deploy natives snapshot
         run: |
-          source .github/actions/tools/bintray.sh
+          source .github/actions/tools/minio.sh
           NATIVE_CHANGES="yes"
           branch="${GITHUB_REF//refs\/heads\//}"
           if [ "$branch" != "" ];
@@ -266,7 +261,7 @@ jobs:
               then
                 echo "Configure the OBJECTS_KEY secret to enable natives snapshot deployment to MinIO"
               else
-                # Deploy natives snapshot to a MinIO instance using function in bintray.sh
+                # Deploy natives snapshot to a MinIO instance using function in minio.sh
                 minio_uploadFile dist/jme3-natives.zip \
                   native-snapshots/$GITHUB_SHA/jme3-natives.zip \
                   https://objects.jmonkeyengine.org \
@@ -297,20 +292,71 @@ jobs:
             fi
           fi
 
+  # This job deploys snapshots on the master branch
+  DeployJavaSnapshot:
+    needs: [BuildJMonkey]
+    name: Deploy Java Snapshot
+    runs-on: ubuntu-latest
+    if: github.event_name == 'push' && github.ref_name == 'master'
+    steps:
+
+      # We need to clone everything again for uploadToMaven.sh ...
+      - name: Clone the repo
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      # Setup jdk 21 used for building Maven-style artifacts
+      - name: Setup the java environment
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '21'
+
+      - name: Download natives for android
+        uses: actions/download-artifact@master
+        with:
+          name: android-natives
+          path: build/native
+
+      - name: Rebuild the maven artifacts and deploy them to the Sonatype repository
+        run: |
+          if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
+          then
+            echo "Configure the following secrets to enable deployment to Sonatype:"
+            echo "OSSRH_PASSWORD, OSSRH_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
+          else
+            ./gradlew publishMavenPublicationToSNAPSHOTRepository \
+            -PossrhPassword=${{ secrets.OSSRH_PASSWORD }} \
+            -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
+            -PsigningKey='${{ secrets.SIGNING_KEY }}' \
+            -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
+            -PuseCommitHashAsVersionName=true \
+            --console=plain --stacktrace
+          fi
+
+
   # This job deploys the release
   DeployRelease:
     needs: [BuildJMonkey]
     name: Deploy Release
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-latest
     if: github.event_name == 'release'
     steps:
 
       # We need to clone everything again for uploadToMaven.sh ...
       - name: Clone the repo
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 1
 
+      # Setup jdk 21 used for building Sonatype OSSRH artifacts
+      - name: Setup the java environment
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '21'
+
       # Download all the stuff...
       - name: Download maven artifacts
         uses: actions/download-artifact@master
@@ -324,6 +370,12 @@ jobs:
           name: release
           path: dist/release
 
+      - name: Download natives for android
+        uses: actions/download-artifact@master
+        with:
+          name: android-natives
+          path: build/native
+
       - name: Rebuild the maven artifacts and deploy them to Sonatype OSSRH
         run: |
           if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
@@ -336,7 +388,7 @@ jobs:
             -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
             -PsigningKey='${{ secrets.SIGNING_KEY }}' \
             -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
-            -PskipPrebuildLibraries=true -PuseCommitHashAsVersionName=true \
+            -PuseCommitHashAsVersionName=true \
             --console=plain --stacktrace
           fi
 
@@ -358,29 +410,18 @@ jobs:
           --data-binary @"$filename" \
           "$url"
 
-      - name: Deploy to bintray
+      - name: Deploy to github package registry
         run: |
           source .github/actions/tools/uploadToMaven.sh
-          if [ "${{ secrets.BINTRAY_MAVEN_REPO }}" = "" ];
-          then
-            echo "Configure the following secrets to enable bintray deployment"
-            echo "BINTRAY_MAVEN_REPO, BINTRAY_USER, BINTRAY_APIKEY"
-          else
-            uploadAllToMaven dist/maven/ https://api.bintray.com/maven/${{ secrets.BINTRAY_MAVEN_REPO }} ${{ secrets.BINTRAY_USER }} ${{ secrets.BINTRAY_APIKEY }} "https://github.com/${GITHUB_REPOSITORY}" "${{ secrets.BINTRAY_LICENSE }}"
-          fi
-
-      # - name: Deploy to github package registry
-      #   run: |
-      #     source .github/actions/tools/uploadToMaven.sh
-      #     registry="https://maven.pkg.github.com/$GITHUB_REPOSITORY"
-      #     echo "Deploy to github package registry $registry"
-      #     uploadAllToMaven dist/maven/ $registry "token" ${{ secrets.GITHUB_TOKEN }}
+          registry="https://maven.pkg.github.com/$GITHUB_REPOSITORY"
+          echo "Deploy to github package registry $registry"
+          uploadAllToMaven dist/maven/ $registry "token" ${{ secrets.GITHUB_TOKEN }}
 
   # Deploy the javadoc
   DeployJavaDoc:
     needs: [BuildJMonkey]
     name: Deploy Javadoc
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-latest
     if: github.event_name == 'release'
     steps:
 

+ 3 - 1
.gitignore

@@ -14,6 +14,7 @@
 /.classpath
 /.project
 /.settings
+/local.properties
 *.dll
 *.so
 *.jnilib
@@ -47,4 +48,5 @@ appveyor.yml
 javadoc_deploy
 javadoc_deploy.pub
 !.vscode/settings.json
-!.vscode/JME_style.xml
+!.vscode/JME_style.xml
+!.vscode/extensions.json

+ 4 - 4
.vscode/JME_style.xml

@@ -43,7 +43,7 @@
 <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
 <setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
 <setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
-<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="110"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
@@ -89,7 +89,7 @@
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
 <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
-<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="180"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="110"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
@@ -220,7 +220,7 @@
 <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
 <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
 <setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
-<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="110"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
 <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
@@ -264,7 +264,7 @@
 <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
 <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
 <setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
-<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="110"/>
 <setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="true"/>
 <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
 <setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+	"recommendations": [
+                "vscjava.vscode-java-pack",
+                "slevesque.shader"
+	]
+}

+ 12 - 2
.vscode/settings.json

@@ -1,5 +1,15 @@
 {
     "java.configuration.updateBuildConfiguration": "automatic",
+    "java.compile.nullAnalysis.mode": "automatic",
     "java.refactor.renameFromFileExplorer": "prompt",
-    "java.format.settings.url": "./.vscode/JME_style.xml"
-}
+    "java.format.settings.url": "./.vscode/JME_style.xml",
+    "editor.formatOnPaste": true,
+    "editor.formatOnType": false,
+    "editor.formatOnSave": true,
+    "editor.formatOnSaveMode": "modifications" ,
+
+    "prettier.tabWidth": 4,
+    "prettier.printWidth": 110,
+    "prettier.enable": true,
+    "prettier.resolveGlobalModules": true
+}

+ 138 - 6
CONTRIBUTING.md

@@ -13,14 +13,92 @@ Check out the [Projects](https://github.com/jMonkeyEngine/jmonkeyengine/projects
 
 When you're ready to submit your code, just make a [pull request](https://help.github.com/articles/using-pull-requests).
 
-- Do not commit your code until you have received proper feedback.
 - In your commit log message, please refer back to the originating forum thread (example) for a ‘full circle’ reference. Also please [reference related issues](https://help.github.com/articles/closing-issues-via-commit-messages) by typing the issue hashtag.
 - When committing, always be sure to run an update before you commit. If there is a conflict between the latest revision and your patch after the update, then it is your responsibility to track down the update that caused the conflict and determine the issue (and fix it). In the case where the breaking commit has no thread linked (and one cannot be found in the forum), then the contributor should contact an administrator and wait for feedback before committing.
 - If your code is committed and it introduces new functionality, please edit the wiki accordingly. We can easily roll back to previous revisions, so just do your best; point us to it and we’ll see if it sticks!
 
 p.s. We will try hold ourselves to a [certain standard](http://www.defmacro.org/2013/04/03/issue-etiquette.html) when it comes to GitHub etiquette. If at any point we fail to uphold this standard, let us know.
 
-#### Core Contributors
+There are many ways
+to submit a pull request (PR) to the "jmonkeyengine" project repository,
+depending on your knowledge of Git and which tools you prefer.
+
+<details>
+    <summary>
+        <b>Click to view step-by-step instructions for a reusable setup
+        using a web browser and a command-line tool such as Bash.</b>
+    </summary>
+
+The setup described here allows you to reuse the same local repo for many PRs.
+
+#### Prerequisites
+
+These steps need only be done once...
+
+1. You'll need a personal account on https://github.com/ .
+   The "Sign up" and "Sign in" buttons are in the upper-right corner.
+2. Create a GitHub access token, if you don't already have one:
+  + Browse to https://github.com/settings/tokens
+  + Click on the "Generate new token" button in the upper right.
+  + Follow the instructions.
+  + When specifying the scope of the token, check the box labeled "repo".
+  + Copy the generated token to a secure location from which you can
+    easily paste it into your command-line tool.
+3. Create your personal fork of the "jmonkeyengine" repository at GitHub,
+   if you don't already have one:
+  + Browse to https://github.com/jMonkeyEngine/jmonkeyengine
+  + Click on the "Fork" button (upper right)
+  + Follow the instructions.
+  + If offered a choice of locations, choose your personal account.
+4. Clone the fork to your development system:
+  + `git clone https://github.com/` ***yourGitHubUserName*** `/jmonkeyengine.git`
+  + As of 2021, this step consumes about 1.3 GBytes of filesystem storage.
+5. Create a local branch for tracking the project repository:
+  + `cd jmonkeyengine`
+  + `git remote add project https://github.com/jMonkeyEngine/jmonkeyengine.git`
+  + `git fetch project`
+  + `git checkout -b project-master project/master`
+
+#### PR process
+
+1. Create a temporary, up-to-date, local branch for your PR changes:
+  + `git checkout project-master`
+  + `git pull`
+  + `git checkout -b tmpBranch` (replace "tmpBranch" with a descriptive name)
+2. Make your changes in the working tree.
+3. Test your changes.
+   Testing should, at a minimum, include building the Engine from scratch:
+  + `./gradlew clean build`
+4. Add and commit your changes to your temporary local branch.
+5. Push the PR commits to your fork at GitHub:
+  + `git push --set-upstream origin ` ***tmpBranchName***
+  + Type your GitHub user name at the "Username" prompt.
+  + Paste your access token (from prerequisite step 2) at the "Password" prompt.
+6. Initiate the pull request:
+  + Browse to [https://github.com/ ***yourGitHubUserName*** /jmonkeyengine]()
+  + Click on the "Compare & pull request" button at the top.
+  + The "base repository:" should be "jMonkeyEngine/jmonkeyengine".
+  + The "base:" should be "master".
+  + The "head repository:" should be your personal fork at GitHub.
+  + The "compare:" should be the name of your temporary branch.
+7. Fill in the textboxes for the PR name and PR description, and
+    click on the "Create pull request" button.
+
+To amend an existing PR:
+  + `git checkout tmpBranch`
+  + Repeat steps 2 through 5.
+
+To submit another PR using the existing local repository,
+repeat the PR process using a new temporary branch with a different name.
+
+If you have an integrated development environment (IDE),
+it may provide an interface to Git that's more intuitive than a command line.
+</details>
+
+Generic instructions for creating GitHub pull requests can be found at
+https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
+
+### Core Contributors
 
 Developers in the Contributors team can push directly to Main instead of submitting pull requests, however for new features it is often a good idea to do a pull request as a means to get a last code review.
 
@@ -33,10 +111,44 @@ Developers in the Contributors team can push directly to Main instead of submitt
 - In general, library changes that plausibly might break existing apps appear only in major releases, not minor ones.
 
 
-## Building the engine
+## How to build the Engine from source
 
-1. Install [Gradle](http://www.gradle.org/)
-2. Navigate to the project directory and run 'gradle build' from command line to build the engine.
+### Prerequisites
+
+These steps need only be done once...
+
+1. Install a Java Development Kit (JDK), if you don't already have one.
+2. Set the JAVA_HOME environment variable:
+  + using Bash: `export JAVA_HOME="` *path to your JDK* `"`
+  + using PowerShell: `$env:JAVA_HOME = '` *path to your JDK* `'`
+  + using Windows Command Prompt: `set JAVA_HOME="` *path to your JDK* `"`
+  + Tip: The path names a directory containing "bin" and "lib" subdirectories.
+    On Linux it might be something like "/usr/lib/jvm/java-17-openjdk-amd64"
+  + Tip: You may be able to skip this step
+    if the JDK binaries are in your system path.
+3. Clone the project repository from GitHub:
+  + `git clone https://github.com/jmonkeyengine/jmonkeyengine.git`
+  + `cd jmonkeyengine`
+  + As of 2021, this step consumes about 1.3 GBytes of filesystem storage.
+
+### Build command
+
+Run the Gradle wrapper:
++ using Bash or PowerShell: `./gradlew build`
++ using Windows Command Prompt: `.\gradlew build`
+
+After a successful build,
+snapshot jars will be found in the "*/build/libs" subfolders.
+
+### Related Gradle tasks
+
+You can install the Maven artifacts to your local repository:
+ + using Bash or PowerShell:  `./gradlew install`
+ + using Windows Command Prompt:  `.\gradlew install`
+
+You can restore the project to a pristine state:
+ + using Bash or PowerShell: `./gradlew clean`
+ + using Windows Command Prompt: `.\gradlew clean`
 
 ## Best Practices
 
@@ -49,9 +161,29 @@ Developers in the Contributors team can push directly to Main instead of submitt
 
 general testing tips? WIP
 
+### Coding Style
+
++ Our preferred style for Java source code is
+  [Google style](https://google.github.io/styleguide/javaguide.html) with the following 8 modifications:
+  1. No blank line before a `package` statement. (Section 3)
+  2. Logical ordering of class contents is encouraged but not required. (Section 3.4.2)
+  3. Block indentation of +4 spaces instead of +2. (Section 4.2)
+  4. Column limit of 110 instead of 100. (Section 4.4)
+  5. Continuation line indentation of +8 spaces instead of +4. (Section 4.5.2)
+  6. Commented-out code need not be indented at the same level as surrounding code. (Section 4.8.6.1)
+  7. The names of test classes need not end in "Test". (Section 5.2.2)
+  8. No trailing whitespace.
++ Any pull request that adds new Java source files shall apply our preferred style to those files.
++ Any pull request that has style improvement as its primary purpose
+  shall apply our preferred style, or specific aspect(s) thereof, to every file it modifies.
++ Any pull request that modifies a pre-existing source file AND
+  doesn't have style improvement as it's primary purpose shall either:
+  1. conform to the prevailing style of that file OR
+  2. apply our preferred style, but only to the portions modified for the PR's primary purpose.
+
 ### Code Quality
 
-We generally abide by the standard Java Code Conventions. Besides that, just make an effort to write elegant code:
+Make an effort to write elegant code:
 
  1. Handles errors gracefully
  2. Only reinvents the wheel when there is a measurable benefit in doing so.

+ 0 - 29
LICENSE

@@ -1,29 +0,0 @@
-Copyright (c) 2009-2021 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.

+ 30 - 0
LICENSE.md

@@ -0,0 +1,30 @@
+Copyright (c) 2009-2024 jMonkeyEngine.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the copyright holder 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 HOLDER 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.

+ 30 - 16
README.md

@@ -3,41 +3,55 @@ jMonkeyEngine
 
 [![Build Status](https://github.com/jMonkeyEngine/jmonkeyengine/workflows/Build%20jMonkeyEngine/badge.svg)](https://github.com/jMonkeyEngine/jmonkeyengine/actions)
 
-jMonkeyEngine is a 3-D game engine for adventurous Java developers. It’s open-source, cross-platform, and cutting-edge. 3.2.4 is the latest stable version of the jMonkeyEngine 3 SDK, a complete game development suite. We'll release 3.2.x updates until the major 3.3 release arrives.
+jMonkeyEngine is a 3-D game engine for adventurous Java developers. It’s open-source, cross-platform, and cutting-edge.
+v3.6.1 is the latest stable version of the engine.
 
 The engine is used by several commercial game studios and computer-science courses. Here's a taste:
 
 ![jME3 Games Mashup](https://i.imgur.com/nF8WOW6.jpg)
 
- - [jME powered games on IndieDB](http://www.indiedb.com/engines/jmonkeyengine/games)
- - [Maker's Tale](http://steamcommunity.com/sharedfiles/filedetails/?id=93461954t)
+ - [jME powered games on IndieDB](https://www.indiedb.com/engines/jmonkeyengine/games)
  - [Boardtastic 2](https://boardtastic-2.fileplanet.com/apk)
  - [Attack of the Gelatinous Blob](https://attack-gelatinous-blob.softwareandgames.com/)
- - [Mythruna](http://mythruna.com/)
+ - [Mythruna](https://mythruna.com/)
  - [PirateHell (on Steam)](https://store.steampowered.com/app/321080/Pirate_Hell/)
- - [3089 (on Steam)](http://store.steampowered.com/app/263360/)
- - [3079 (on Steam)](http://store.steampowered.com/app/259620/)
+ - [3089 (on Steam)](https://store.steampowered.com/app/263360/3089__Futuristic_Action_RPG/)
+ - [3079 (on Steam)](https://store.steampowered.com/app/259620/3079__Block_Action_RPG/)
  - [Lightspeed Frontier (on Steam)](https://store.steampowered.com/app/548650/Lightspeed_Frontier/)
  - [Skullstone](http://www.skullstonegame.com/)
  - [Spoxel (on Steam)](https://store.steampowered.com/app/746880/Spoxel/)
  - [Nine Circles of Hell (on Steam)](https://store.steampowered.com/app/1200600/Nine_Circles_of_Hell/)
  - [Leap](https://gamejolt.com/games/leap/313308)
  - [Jumping Jack Flag](http://timealias.bplaced.net/jack/)
-
-## Getting started
+ - [PapaSpace Flight Simulation](https://www.papaspace.at/)
+ - [Cubic Nightmare (on Itch)](https://jaredbgreat.itch.io/cubic-nightmare)
+ - [Chatter Games](https://chatter-games.com)
+ - [Exotic Matter](https://exoticmatter.io)
+ - [Demon Lord (on Google Play)](https://play.google.com/store/apps/details?id=com.dreiInitiative.demonLord&pli=1)
+ - [Marvelous Marbles (on Steam)](https://store.steampowered.com/app/2244540/Marvelous_Marbles/)
+ - [Boxer (on Google Play)](https://play.google.com/store/apps/details?id=com.tharg.boxer)
+ - [Depthris (on Itch)](https://codewalker.itch.io/depthris)
+ - [Stranded (on Itch)](https://tgiant.itch.io/stranded)
+ - [The Afflicted Forests (Coming Soon to Steam)](https://www.indiedb.com/games/the-afflicted-forests)
+ - [Star Colony: Beyond Horizons (on Google Play)](https://play.google.com/store/apps/details?id=game.colony.ColonyBuilder)
+ - [High Impact (on Steam)](https://store.steampowered.com/app/3059050/High_Impact/)
+
+## Getting Started
 
 Go to https://github.com/jMonkeyEngine/sdk/releases to download the jMonkeyEngine SDK.
-[Read the wiki](https://jmonkeyengine.github.io/wiki) for a complete install guide. Power up with some SDK Plugins and AssetPacks and you are off to the races. At this point you're gonna want to [join the forum](http://hub.jmonkeyengine.org/) so our tribe can grow stronger.
+Read [the wiki](https://jmonkeyengine.github.io/wiki) for the installation guide and tutorials.
+Join [the discussion forum](https://hub.jmonkeyengine.org/) to participate in our community,
+get your questions answered, and share your projects.
 
-Note: The master branch on GitHub is a development version of the engine and is NOT MEANT TO BE USED IN PRODUCTION, it will break constantly during development of the stable jME versions!
+Note: The master branch on GitHub is a development version of the engine and is NOT MEANT TO BE USED IN PRODUCTION.
 
 ### Technology Stack
 
- - Java
- - NetBeans Platform
- - Gradle
-
-Plus a bunch of awesome libraries & tight integrations like Bullet, NiftyGUI and other goodies.
+ - windowed, multi-platform IDE derived from NetBeans
+ - libraries for GUI, networking, physics, SFX, terrain, importing assets, etc.
+ - platform-neutral core library for scene graph, animation, rendering, math, etc.
+ - LWJGL v2/v3 (to access GLFW, OpenAL, OpenGL, and OpenVR) or Android or iOS
+ - Java Virtual Machine (v8 or higher)
 
 ### Documentation
 
@@ -49,5 +63,5 @@ Read our [contribution guide](https://github.com/jMonkeyEngine/jmonkeyengine/blo
 
 ### License
 
-New BSD (3-clause) License. In other words, you do whatever makes you happy!
+[New BSD (3-clause) License](https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/LICENSE.md)
 

+ 0 - 29
bintray.gradle

@@ -1,29 +0,0 @@
-//
-// This file is to be applied to some subproject.
-//
-
-apply plugin: 'com.jfrog.bintray'
-
-bintray {
-    user = bintray_user
-    key = bintray_api_key
-    configurations = ['archives']
-    dryRun = false 
-    pkg {
-        repo = 'org.jmonkeyengine'
-        userOrg = 'jmonkeyengine'
-        name = project.name
-        desc = POM_DESCRIPTION
-        websiteUrl = POM_URL
-        licenses = ['BSD New']
-        vcsUrl = POM_SCM_URL
-        labels = ['jmonkeyengine']
-    }
-}
-
-bintrayUpload.dependsOn(writeFullPom)
-
-bintrayUpload.onlyIf {
-    (bintray_api_key.length() > 0) &&
-    !(version ==~ /.*SNAPSHOT/)
-}

+ 45 - 76
build.gradle

@@ -3,29 +3,31 @@ import java.nio.file.StandardCopyOption;
 
 buildscript {
     repositories {
+        mavenCentral()
         google()
-        jcenter()
         maven {
             url "https://plugins.gradle.org/m2/"
         }
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.3'
-        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
-        classpath 'me.tatarka:gradle-retrolambda:3.7.1'
-        classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.5.1"
+        classpath libs.android.build.gradle
+        classpath libs.gradle.retrolambda
+        classpath libs.spotbugs.gradle.plugin
     }
 }
 
 allprojects {
     repositories {
+        mavenCentral()
         google()
-        jcenter()
+    }
+    tasks.withType(Jar) {
+        duplicatesStrategy = 'include'
     }
 }
 
 // Set the license for IDEs that understand this
-ext.license = file("$rootDir/license.txt")
+ext.license = file("$rootDir/source-file-header-template.txt")
 
 apply plugin: 'base'
 apply plugin: 'com.github.spotbugs'
@@ -37,9 +39,6 @@ apply plugin: 'me.tatarka.retrolambda'
 subprojects {
     if(!project.name.equals('jme3-android-examples')) {
         apply from: rootProject.file('common.gradle')
-        if (!project.name.equals('jme3-testdata')) {
-            apply from: rootProject.file('bintray.gradle')
-        }
     } else {
         apply from: rootProject.file('common-android-app.gradle')
     }
@@ -50,6 +49,7 @@ subprojects {
         // Currently we only warn about issues and try to fix them as we go, but those aren't mission critical.
         spotbugs {
             ignoreFailures = true
+            toolVersion = '4.8.6'
         }
 
         tasks.withType(com.github.spotbugs.snom.SpotBugsTask ) {
@@ -73,25 +73,25 @@ task libDist(dependsOn: subprojects.build, description: 'Builds and copies the e
         File sourceFolder = mkdir("$buildDir/libDist/sources")
         File javadocFolder = mkdir("$buildDir/libDist/javadoc")
         subprojects.each {project ->
-            if(project.ext.mainClass == ''){
+            if(!project.hasProperty('mainClassName')){
                 project.tasks.withType(Jar).each {archiveTask ->
-                    if(archiveTask.classifier == "sources"){
+                    if(archiveTask.archiveClassifier == "sources"){
                         copy {
                             from archiveTask.archivePath
                                 into sourceFolder
-                                rename {project.name + '-' + archiveTask.classifier +'.'+ archiveTask.extension}
+                                rename {project.name + '-' + archiveTask.archiveClassifier +'.'+ archiveTask.archiveExtension}
                         }
-                    } else if(archiveTask.classifier == "javadoc"){
+                    } else if(archiveTask.archiveClassifier == "javadoc"){
                         copy {
                             from archiveTask.archivePath
                                 into javadocFolder
-                                rename {project.name + '-' + archiveTask.classifier +'.'+ archiveTask.extension}
+                                rename {project.name + '-' + archiveTask.archiveClassifier +'.'+ archiveTask.archiveExtension}
                         }
                     } else{
                         copy {
                             from archiveTask.archivePath
                                 into libFolder
-                                rename {project.name + '.' + archiveTask.extension}
+                                rename {project.name + '.' + archiveTask.archiveExtension}
                         }
                     }
                 }
@@ -101,7 +101,9 @@ task libDist(dependsOn: subprojects.build, description: 'Builds and copies the e
 }
 
 task createZipDistribution(type:Zip,dependsOn:["dist","libDist"], description:"Package the nightly zip distribution"){
-    archiveName "jME" + jmeFullVersion + ".zip"
+    archiveFileName = provider {
+        "jME" + jmeFullVersion + ".zip"
+    }
     into("/") {
          from {"./dist"}
     }
@@ -113,7 +115,7 @@ task createZipDistribution(type:Zip,dependsOn:["dist","libDist"], description:"P
 task copyLibs(type: Copy){
 //    description 'Copies the engine dependencies to build/libDist'
     from {
-        subprojects*.configurations*.compile*.copyRecursive({ !(it instanceof ProjectDependency); })*.resolve()
+        subprojects*.configurations*.implementation*.copyRecursive({ !(it instanceof ProjectDependency); })*.resolve()
     }
 
     into "$buildDir/libDist/lib-ext" //buildDir.path + '/' + libsDirName + '/lib'
@@ -123,6 +125,22 @@ task dist(dependsOn: [':jme3-examples:dist', 'mergedJavadoc']){
     description 'Creates a jME3 examples distribution with all jme3 binaries, sources, javadoc and external libraries under ./dist'
 }
 
+def mergedJavadocSubprojects = [
+        ":jme3-android",
+        ":jme3-core",
+        ":jme3-desktop",
+        ":jme3-effects",
+        ":jme3-ios",
+        ":jme3-jbullet",
+        ":jme3-jogg",
+        ":jme3-lwjgl",
+        ":jme3-lwjgl3",
+        ":jme3-networking",
+        ":jme3-niftygui",
+        ":jme3-plugins",
+        ":jme3-terrain",
+        ":jme3-vr"
+]
 task mergedJavadoc(type: Javadoc, description: 'Creates Javadoc from all the projects.') {
     title = 'jMonkeyEngine3'
     destinationDir = mkdir("dist/javadoc")
@@ -135,15 +153,8 @@ task mergedJavadoc(type: Javadoc, description: 'Creates Javadoc from all the pro
     }
 
     options.overview = file("javadoc-overview.html")
-    // Note: The closures below are executed lazily.
-    source subprojects.collect {project ->
-        project.sourceSets.main.allJava // main only, exclude tests
-    }
-    classpath = files(subprojects.collect {project ->
-            project.sourceSets*.compileClasspath})
-    classpath.from {
-        subprojects*.configurations*.compile*.copyRecursive({ !(it instanceof ProjectDependency); })*.resolve()
-    }
+    source = mergedJavadocSubprojects.collect { project(it).sourceSets.main.allJava }
+    classpath = files(mergedJavadocSubprojects.collect { project(it).sourceSets.main.compileClasspath })
 }
 
 clean.dependsOn('cleanMergedJavadoc')
@@ -185,18 +196,18 @@ gradle.rootProject.ext.set("usePrebuildNatives", buildNativeProjects!="true");
 if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
     String rootPath = rootProject.projectDir.absolutePath
 
-    Properties nativesSnasphotProp = new Properties()
-    File nativesSnasphotPropF = new File("${rootPath}/natives-snapshot.properties");
+    Properties nativesSnapshotProp = new Properties()
+    File nativesSnapshotPropF = new File("${rootPath}/natives-snapshot.properties");
 
-    if (nativesSnasphotPropF.exists()) {
+    if (nativesSnapshotPropF.exists()) {
 
-        nativesSnasphotPropF.withInputStream { nativesSnasphotProp.load(it) }
+        nativesSnapshotPropF.withInputStream { nativesSnapshotProp.load(it) }
 
-        String nativesSnasphot = nativesSnasphotProp.getProperty("natives.snapshot");
-        String nativesUrl = PREBUILD_NATIVES_URL.replace('${natives.snapshot}', nativesSnasphot)
+        String nativesSnapshot = nativesSnapshotProp.getProperty("natives.snapshot");
+        String nativesUrl = PREBUILD_NATIVES_URL.replace('${natives.snapshot}', nativesSnapshot)
         println "Use natives snapshot: " + nativesUrl
 
-        String nativesZipFile = "${rootPath}" + File.separator + "build" + File.separator + nativesSnasphot + "-natives.zip"
+        String nativesZipFile = "${rootPath}" + File.separator + "build" + File.separator + nativesSnapshot + "-natives.zip"
         String nativesPath = "${rootPath}" + File.separator + "build" + File.separator + "native"
 
 
@@ -235,48 +246,6 @@ if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
     }
 }
 
-
-//class IncrementalReverseTask extends DefaultTask {
-//    @InputDirectory
-//    def File inputDir
-//
-//    @OutputDirectory
-//    def File outputDir
-//
-//    @Input
-//    def inputProperty
-//
-//    @TaskAction
-//    void execute(IncrementalTaskInputs inputs) {
-//        println inputs.incremental ? "CHANGED inputs considered out of date" : "ALL inputs considered out of date"
-//        inputs.outOfDate { change ->
-//            println "out of date: ${change.file.name}"
-//            def targetFile = new File(outputDir, change.file.name)
-//            targetFile.text = change.file.text.reverse()
-//        }
-//
-//        inputs.removed { change ->
-//            println "removed: ${change.file.name}"
-//            def targetFile = new File(outputDir, change.file.name)
-//            targetFile.delete()
-//        }
-//    }
-//}
-
-//allprojects {
-//    tasks.withType(JavaExec) {
-//        enableAssertions = true // false by default
-//    }
-//    tasks.withType(Test) {
-//        enableAssertions = true // true by default
-//    }
-//}
-
-wrapper {
-    gradleVersion = '5.6.4'
-}
-
-
 retrolambda {
   javaVersion JavaVersion.VERSION_1_7
   incremental true

+ 59 - 45
common.gradle

@@ -2,36 +2,35 @@
 // This file is to be applied to every subproject.
 //
 
-apply plugin: 'java'
+apply plugin: 'java-library'
 apply plugin: 'groovy'
-apply plugin: 'maven'
 apply plugin: 'maven-publish'
 apply plugin: 'signing'
+apply plugin: 'eclipse'
+apply plugin: 'checkstyle'
 
+eclipse.jdt.file.withProperties { props ->
+    props.setProperty "org.eclipse.jdt.core.circularClasspath", "warning"
+}
 group = 'org.jmonkeyengine'
 version = jmeFullVersion
 
-sourceCompatibility = '1.8'
-[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
-
-if(JavaVersion.current() >= JavaVersion.VERSION_1_9) {
-    compileJava {
-	options.compilerArgs.addAll(['--release', '8'])
-        //Replace previous with "options.release = 8" if updated to gradle 6.6 or newer
-    }
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
 }
 
-gradle.projectsEvaluated {
-    tasks.withType(JavaCompile) { // compile-time options:
-        options.compilerArgs << '-Xlint:unchecked'
+tasks.withType(JavaCompile) { // compile-time options:
+    //options.compilerArgs << '-Xlint:deprecation' // to show deprecation warnings
+    options.compilerArgs << '-Xlint:unchecked'
+    options.encoding = 'UTF-8'
+    if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_1_10)) {
+        options.release = 8
     }
 }
 
 repositories {
     mavenCentral()
-    maven {
-        url "http://nifty-gui.sourceforge.net/nifty-maven-repo"
-    }
     flatDir {
         dirs rootProject.file('lib')
     }
@@ -39,13 +38,11 @@ repositories {
 
 dependencies {
     // Adding dependencies here will add the dependencies to each subproject.
-    testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.mockito', name: 'mockito-core', version: '3.0.0'
-    testCompile group: 'org.easytesting', name: 'fest-assert-core', version: '2.0M10'
-    testCompile 'org.codehaus.groovy:groovy-all:2.5.8'
+    testImplementation libs.junit4
+    testImplementation libs.mokito.core
+    testImplementation libs.groovy.test
 }
 
-
 // Uncomment if you want to see the status of every test that is run and
 // the test output.
 /*
@@ -60,7 +57,8 @@ jar {
     manifest {
         attributes 'Implementation-Title': 'jMonkeyEngine',
                    'Implementation-Version': jmeFullVersion,
-                   'Automatic-Module-Name': "${project.name.replace("-", ".")}" 
+                   'Automatic-Module-Name': "${project.name.replace("-", ".")}",
+                   'Created-By': "${JavaVersion.current()} (${System.getProperty("java.vendor")})"
     }
 }
 
@@ -74,10 +72,6 @@ javadoc {
     options.use = "true"
     options.charSet = "UTF-8"
     options.encoding = "UTF-8"
-    //disable doclint for JDK8, more quiet output
-    if (JavaVersion.current().isJava8Compatible()){
-        options.addStringOption('Xdoclint:none', '-quiet')
-    }
     source = sourceSets.main.allJava // main only, exclude tests
 }
 
@@ -88,12 +82,12 @@ test {
 }
 
 task sourcesJar(type: Jar, dependsOn: classes, description: 'Creates a jar from the source files.') {
-    classifier = 'sources'
+    archiveClassifier = 'sources'
     from sourceSets*.allSource
 }
 
 task javadocJar(type: Jar, dependsOn: javadoc, description: 'Creates a jar from the javadoc files.') {
-    classifier = 'javadoc'
+    archiveClassifier = 'javadoc'
     from javadoc.destinationDir
 }
 
@@ -122,28 +116,12 @@ ext.pomConfig = {
     }
 }
 
-// workaround to be able to use same custom pom with 'maven' and 'bintray' plugin
-task writeFullPom {
-    ext.pomFile = "$mavenPomDir/${project.name}-${project.version}.pom"
-    outputs.file pomFile
-    doLast {
-        pom {
-            project pomConfig
-        }.writeTo(pomFile)
-    }
-}
-
-assemble.dependsOn(writeFullPom)
-install.dependsOn(writeFullPom)
-uploadArchives.dependsOn(writeFullPom)
-
 artifacts {
     archives jar
     archives sourcesJar
     if (buildJavaDoc == "true") {
         archives javadocJar
     }
-    archives writeFullPom.outputs.files[0]
 }
 
 publishing {
@@ -190,11 +168,26 @@ publishing {
                 password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
             }
             name = 'OSSRH'
-            url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
+            url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2'
         }
+	maven {
+	    credentials {
+                username = gradle.rootProject.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user'
+                password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
+	    }
+	    name = 'SNAPSHOT'
+	    url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
+	}
     }
 }
 
+publishToMavenLocal.doLast {
+    println 'published ' + project.getName() + "-${jmeFullVersion} to mavenLocal"
+}
+task('install') {
+    dependsOn 'publishToMavenLocal'
+}
+
 signing {
     def signingKey = gradle.rootProject.findProperty('signingKey')
     def signingPassword = gradle.rootProject.findProperty('signingPassword')
@@ -206,3 +199,24 @@ signing {
 tasks.withType(Sign) {
     onlyIf { gradle.rootProject.hasProperty('signingKey') }
 }
+
+checkstyle {
+    toolVersion libs.versions.checkstyle.get()
+    configFile file("${gradle.rootProject.rootDir}/config/checkstyle/checkstyle.xml")
+}
+
+checkstyleMain {
+    source ='src/main/java'
+}
+
+checkstyleTest {
+    source ='src/test/java'
+}
+
+tasks.withType(Checkstyle) {
+    reports {
+        xml.required.set(false)
+        html.required.set(true)
+    }
+    include("**/com/jme3/renderer/**/*.java")
+}

+ 6 - 0
config/checkstyle/checkstyle-suppressions.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<!DOCTYPE suppressions PUBLIC "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" "https://checkstyle.org/dtds/suppressions_1_2.dtd">
+
+<suppressions>
+    <!-- https://checkstyle.org/filters/suppressionfilter.html -->
+</suppressions>

+ 361 - 0
config/checkstyle/checkstyle.xml

@@ -0,0 +1,361 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
+
+<!--
+    Checkstyle configuration that checks the Google coding conventions from Google Java Style
+    that can be found at https://google.github.io/styleguide/javaguide.html. 
+
+    Based on google_checks.xml found in the checkstyle repository:
+    https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml
+
+    Adapted for changes by the jMonkeyEngine contribution guidelines document.
+ -->
+
+<module name="Checker">
+    <property name="charset" value="UTF-8" />
+
+    <property name="severity" value="warning" />
+
+    <property name="fileExtensions" value="java, properties, xml" />
+    <!-- Excludes all 'module-info.java' files              -->
+    <!-- See https://checkstyle.org/filefilters/index.html -->
+    <module name="BeforeExecutionExclusionFileFilter">
+        <property name="fileNamePattern" value="module\-info\.java$" />
+    </module>
+
+    <!-- https://checkstyle.org/filters/suppressionfilter.html -->
+    <module name="SuppressionFilter">
+        <property name="file" value="config/checkstyle/checkstyle-suppressions.xml"/>
+        <property name="optional" value="true" />
+    </module>
+
+    <!-- Checks for whitespace                               -->
+    <!-- See https://checkstyle.org/checks/whitespace/index.html -->
+    <module name="FileTabCharacter">
+        <property name="eachLine" value="false" />
+    </module>
+
+    <module name="LineLength">
+        <property name="fileExtensions" value="java" />
+        <property name="max" value="110" />
+        <property name="ignorePattern"
+            value="^package.*|^import.*|a href|href|http://|https://|ftp://" />
+    </module>
+
+    <module name="TreeWalker">
+        <module name="OuterTypeFilename" />
+        <module name="IllegalTokenText">
+            <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL" />
+            <property name="format"
+                value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)" />
+            <property name="message"
+                value="Consider using special escape sequence instead of octal value or Unicode escaped value." />
+        </module>
+        <module name="AvoidEscapedUnicodeCharacters">
+            <property name="allowEscapesForControlCharacters" value="true" />
+            <property name="allowByTailComment" value="true" />
+            <property name="allowNonPrintableEscapes" value="true" />
+        </module>
+        <module name="AvoidStarImport"/>
+        <module name="OneTopLevelClass" />
+        <module name="NoLineWrap">
+            <property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT" />
+        </module>
+        <module name="EmptyBlock">
+            <property name="option" value="TEXT" />
+            <property name="tokens"
+                value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH" />
+        </module>
+        <module name="NeedBraces">
+            <property name="tokens"
+                value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE" />
+        </module>
+        <module name="LeftCurly">
+            <property name="tokens"
+                value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
+                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
+                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
+                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
+                    OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF" />
+        </module>
+        <module name="RightCurly">
+            <property name="id" value="RightCurlySame" />
+            <property name="tokens"
+                value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
+                    LITERAL_DO" />
+        </module>
+        <module name="RightCurly">
+            <property name="id" value="RightCurlyAlone" />
+            <property name="option" value="alone" />
+            <property name="tokens"
+                value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
+                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
+                    COMPACT_CTOR_DEF" />
+        </module>
+        <module name="SuppressionXpathSingleFilter">
+            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
+            <property name="id" value="RightCurlyAlone" />
+            <property name="query"
+                value="//RCURLY[parent::SLIST[count(./*)=1]
+                                     or preceding-sibling::*[last()][self::LCURLY]]" />
+        </module>
+        <module name="WhitespaceAfter">
+            <property name="tokens"
+                value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
+                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE" />
+        </module>
+        <module name="WhitespaceAround">
+            <property name="allowEmptyConstructors" value="true" />
+            <property name="allowEmptyLambdas" value="true" />
+            <property name="allowEmptyMethods" value="true" />
+            <property name="allowEmptyTypes" value="true" />
+            <property name="allowEmptyLoops" value="true" />
+            <property name="ignoreEnhancedForColon" value="false" />
+            <property name="tokens"
+                value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
+                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
+                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
+                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
+                    LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
+                    NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
+                    SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND" />
+            <message key="ws.notFollowed"
+                value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)" />
+            <message key="ws.notPreceded"
+                value="WhitespaceAround: ''{0}'' is not preceded with whitespace." />
+        </module>
+        <module name="OneStatementPerLine" />
+        <module name="MultipleVariableDeclarations" />
+        <module name="ArrayTypeStyle" />
+        <module name="MissingSwitchDefault" />
+        <module name="FallThrough" />
+        <module name="UpperEll" />
+        <module name="ModifierOrder" />
+        <module name="EmptyLineSeparator">
+            <property name="tokens"
+                value="IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
+                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
+                    COMPACT_CTOR_DEF" />
+            <property name="allowNoEmptyLineBetweenFields" value="true" />
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapDot" />
+            <property name="tokens" value="DOT" />
+            <property name="option" value="nl" />
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapComma" />
+            <property name="tokens" value="COMMA" />
+            <property name="option" value="EOL" />
+        </module>
+        <module name="SeparatorWrap">
+            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
+            <property name="id" value="SeparatorWrapEllipsis" />
+            <property name="tokens" value="ELLIPSIS" />
+            <property name="option" value="EOL" />
+        </module>
+        <module name="SeparatorWrap">
+            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
+            <property name="id" value="SeparatorWrapArrayDeclarator" />
+            <property name="tokens" value="ARRAY_DECLARATOR" />
+            <property name="option" value="EOL" />
+        </module>
+        <module name="SeparatorWrap">
+            <property name="id" value="SeparatorWrapMethodRef" />
+            <property name="tokens" value="METHOD_REF" />
+            <property name="option" value="nl" />
+        </module>
+        <module name="PackageName">
+            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />
+            <message key="name.invalidPattern"
+                value="Package name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="TypeName">
+            <property name="tokens"
+                value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
+                    ANNOTATION_DEF, RECORD_DEF" />
+            <message key="name.invalidPattern"
+                value="Type name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="MemberName">
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$" />
+            <message key="name.invalidPattern"
+                value="Member name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="ParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Parameter name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="LambdaParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Lambda parameter name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="CatchParameterName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Catch parameter name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="LocalVariableName">
+            <property name="format" value="^[a-z]([a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Local variable name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="PatternVariableName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Pattern variable name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="ClassTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)" />
+            <message key="name.invalidPattern"
+                value="Class type name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="RecordComponentName">
+            <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$" />
+            <message key="name.invalidPattern"
+                value="Record component name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="RecordTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)" />
+            <message key="name.invalidPattern"
+                value="Record type name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="MethodTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)" />
+            <message key="name.invalidPattern"
+                value="Method type name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="InterfaceTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)" />
+            <message key="name.invalidPattern"
+                value="Interface type name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="NoFinalizer" />
+        <module name="GenericWhitespace">
+            <message key="ws.followed"
+                value="GenericWhitespace ''{0}'' is followed by whitespace." />
+            <message key="ws.preceded"
+                value="GenericWhitespace ''{0}'' is preceded with whitespace." />
+            <message key="ws.illegalFollow"
+                value="GenericWhitespace ''{0}'' should followed by whitespace." />
+            <message key="ws.notPreceded"
+                value="GenericWhitespace ''{0}'' is not preceded with whitespace." />
+        </module>
+        <module name="Indentation">
+            <property name="basicOffset" value="4" />
+            <property name="braceAdjustment" value="4" />
+            <property name="caseIndent" value="4" />
+            <property name="throwsIndent" value="4" />
+            <property name="lineWrappingIndentation" value="8" />
+            <property name="arrayInitIndent" value="4" />
+        </module>
+        <module name="AbbreviationAsWordInName">
+            <property name="ignoreFinal" value="false" />
+            <property name="allowedAbbreviationLength" value="1" />
+            <property name="tokens"
+                value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
+                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
+                    RECORD_COMPONENT_DEF" />
+        </module>
+        <module name="NoWhitespaceBeforeCaseDefaultColon" />
+        <module name="VariableDeclarationUsageDistance" />
+        <module name="CustomImportOrder">
+            <property name="sortImportsInGroupAlphabetically" value="true" />
+            <property name="separateLineBetweenGroups" value="true" />
+            <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE" />
+            <property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF" />
+        </module>
+        <module name="MethodParamPad">
+            <property name="tokens"
+                value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
+                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF" />
+        </module>
+        <module name="NoWhitespaceBefore">
+            <property name="tokens"
+                value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
+                    LABELED_STAT, METHOD_REF" />
+            <property name="allowLineBreaks" value="true" />
+        </module>
+        <module name="ParenPad">
+            <property name="tokens"
+                value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
+                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
+                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
+                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
+                    RECORD_DEF" />
+        </module>
+        <module name="OperatorWrap">
+            <property name="option" value="NL" />
+            <property name="tokens"
+                value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
+                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
+                    TYPE_EXTENSION_AND " />
+        </module>
+        <module name="AnnotationLocation">
+            <property name="id" value="AnnotationLocationMostCases" />
+            <property name="tokens"
+                value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
+                      RECORD_DEF, COMPACT_CTOR_DEF" />
+        </module>
+        <module name="AnnotationLocation">
+            <property name="id" value="AnnotationLocationVariables" />
+            <property name="tokens" value="VARIABLE_DEF" />
+            <property name="allowSamelineMultipleAnnotations" value="true" />
+        </module>
+        <module name="NonEmptyAtclauseDescription" />
+
+        <!--
+
+        Commented out javadoc checks, these produce a lot (4k in jme3-core) of warnings so for now, might
+        be too noisy
+
+        <module name="InvalidJavadocPosition" />
+        <module name="JavadocTagContinuationIndentation" />
+        <module name="SummaryJavadoc">
+            <property name="forbiddenSummaryFragments"
+                value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )" />
+        </module>
+        <module name="JavadocParagraph" />
+        <module name="RequireEmptyLineBeforeBlockTagGroup" />
+        <module name="AtclauseOrder">
+            <property name="tagOrder" value="@param, @return, @throws, @deprecated" />
+            <property name="target"
+                value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF" />
+        </module>
+        <module name="JavadocMethod">
+            <property name="accessModifiers" value="public" />
+            <property name="allowMissingParamTags" value="true" />
+            <property name="allowMissingReturnTag" value="true" />
+            <property name="allowedAnnotations" value="Override, Test" />
+            <property name="tokens"
+                value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF" />
+        </module>
+        <module name="MissingJavadocMethod">
+            <property name="scope" value="public" />
+            <property name="minLineCount" value="2" />
+            <property name="allowedAnnotations" value="Override, Test" />
+            <property name="tokens"
+                value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
+                                   COMPACT_CTOR_DEF" />
+        </module>
+        <module name="MissingJavadocType">
+            <property name="scope" value="protected" />
+            <property name="tokens"
+                value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
+                      RECORD_DEF, ANNOTATION_DEF" />
+            <property name="excludeScope" value="nothing" />
+        </module>
+        <module name="MethodName">
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$" />
+            <message key="name.invalidPattern"
+                value="Method name ''{0}'' must match pattern ''{1}''." />
+        </module>
+        <module name="SingleLineJavadoc" />
+    -->
+        <module name="EmptyCatchBlock">
+            <property name="exceptionVariableName" value="expected" />
+        </module>
+    </module>
+</module>

+ 1 - 5
gradle.properties

@@ -1,5 +1,5 @@
 # Version number: Major.Minor.SubMinor (e.g. 3.3.0)
-jmeVersion = 3.4.0
+jmeVersion = 3.8.0
 
 # Leave empty to autogenerate
 # (use -PjmeVersionName="myVersion" from commandline to specify a custom version name )
@@ -42,8 +42,4 @@ POM_LICENSE_URL=http://opensource.org/licenses/BSD-3-Clause
 POM_LICENSE_DISTRIBUTION=repo
 POM_INCEPTION_YEAR=2009
 
-# Bintray settings to override in $HOME/.gradle/gradle.properties or ENV or commandline
-bintray_user=
-bintray_api_key=
-
 PREBUILD_NATIVES_URL=https://objects.jmonkeyengine.org/native-snapshots/${natives.snapshot}/jme3-natives.zip

+ 50 - 0
gradle/libs.versions.toml

@@ -0,0 +1,50 @@
+## catalog of libraries and plugins used to build the jmonkeyengine project
+
+[versions]
+
+checkstyle = "9.3"
+lwjgl3 = "3.3.3"
+nifty = "1.4.3"
+
+[libraries]
+
+android-build-gradle = "com.android.tools.build:gradle:4.2.0"
+android-support-appcompat = "com.android.support:appcompat-v7:28.0.0"
+androidx-annotation = "androidx.annotation:annotation:1.3.0"
+androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.4.0"
+gradle-git = "org.ajoberstar:gradle-git:1.2.0"
+gradle-retrolambda = "me.tatarka:gradle-retrolambda:3.7.1"
+groovy-test = "org.codehaus.groovy:groovy-test:3.0.21"
+gson = "com.google.code.gson:gson:2.9.1"
+j-ogg-vorbis = "com.github.stephengold:j-ogg-vorbis:1.0.4"
+jbullet = "com.github.stephengold:jbullet:1.0.3"
+jinput = "net.java.jinput:jinput:2.0.9"
+jna = "net.java.dev.jna:jna:5.10.0"
+jnaerator-runtime = "com.nativelibs4java:jnaerator-runtime:0.12"
+junit4 = "junit:junit:4.13.2"
+lwjgl2 = "org.jmonkeyengine:lwjgl:2.9.5"
+lwjgl3-awt = "org.lwjglx:lwjgl3-awt:0.1.8"
+
+lwjgl3-base     = { module = "org.lwjgl:lwjgl",          version.ref = "lwjgl3" }
+lwjgl3-glfw     = { module = "org.lwjgl:lwjgl-glfw",     version.ref = "lwjgl3" }
+lwjgl3-jawt     = { module = "org.lwjgl:lwjgl-jawt",     version.ref = "lwjgl3" }
+lwjgl3-jemalloc = { module = "org.lwjgl:lwjgl-jemalloc", version.ref = "lwjgl3" }
+lwjgl3-openal   = { module = "org.lwjgl:lwjgl-openal",   version.ref = "lwjgl3" }
+lwjgl3-opencl   = { module = "org.lwjgl:lwjgl-opencl",   version.ref = "lwjgl3" }
+lwjgl3-opengl   = { module = "org.lwjgl:lwjgl-opengl",   version.ref = "lwjgl3" }
+lwjgl3-openvr   = { module = "org.lwjgl:lwjgl-openvr",   version.ref = "lwjgl3" }
+lwjgl3-ovr      = { module = "org.lwjgl:lwjgl-ovr",      version.ref = "lwjgl3" }
+
+mokito-core = "org.mockito:mockito-core:3.12.4"
+
+nifty                  = { module = "com.github.nifty-gui:nifty",                  version.ref = "nifty" }
+nifty-default-controls = { module = "com.github.nifty-gui:nifty-default-controls", version.ref = "nifty" }
+nifty-examples         = { module = "com.github.nifty-gui:nifty-examples",         version.ref = "nifty" }
+nifty-style-black      = { module = "com.github.nifty-gui:nifty-style-black",      version.ref = "nifty" }
+
+spotbugs-gradle-plugin = "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.18"
+vecmath = "javax.vecmath:vecmath:1.5.2"
+
+[bundles]
+
+[plugins]

BIN
gradle/wrapper/gradle-wrapper.jar


+ 3 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 172 - 111
gradlew

@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
 
 #
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,78 +17,111 @@
 #
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
 
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
+
 # Determine the Java command to use to start the JVM.
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
 
 # For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=$((i+1))
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=$(save "$@")
 
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
 fi
 
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
 exec "$JAVACMD" "$@"

+ 25 - 33
gradlew.bat

@@ -14,7 +14,7 @@
 @rem limitations under the License.
 @rem
 
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
 @rem ##########################################################################
 @rem
 @rem  Gradle startup script for Windows
@@ -25,10 +25,14 @@
 if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
 @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
@@ -37,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
 
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -51,48 +55,36 @@ goto fail
 set JAVA_HOME=%JAVA_HOME:"=%
 set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
 
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
 :execute
 @rem Setup the command line
 
 set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
+
 @rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
 
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
 
 :mainEnd
 if "%OS%"=="Windows_NT" endlocal

+ 1 - 0
jme-angle/src/native/angle

@@ -0,0 +1 @@
+Subproject commit 2319607679d7781ff9bab5e821a34574ecb0bcc3

+ 15 - 21
jme3-android-examples/build.gradle

@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 28
-    buildToolsVersion "28.0.3"
+    buildToolsVersion "30.0.2"
 
     lintOptions {
         // Fix nifty gui referencing "java.awt" package.
@@ -25,11 +25,6 @@ android {
         }
     }
 
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-
     sourceSets {
         main {
             java {
@@ -45,19 +40,18 @@ android {
 }
 
 dependencies {
-    compile fileTree(dir: 'libs', include: ['*.jar'])
-    testCompile 'junit:junit:4.12'
-    compile 'com.android.support:appcompat-v7:28.0.0'
-
-    compile project(':jme3-core')
-    compile project(':jme3-android')
-    compile project(':jme3-android-native')
-    compile project(':jme3-effects')
-    compile project(':jme3-jbullet')
-    compile project(':jme3-networking')
-    compile project(':jme3-niftygui')
-    compile project(':jme3-plugins')
-    compile project(':jme3-terrain')
-    compile fileTree(dir: '../jme3-examples/build/libs', include: ['*.jar'], exclude: ['*sources*.*'])
-//    compile project(':jme3-examples')
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    testImplementation libs.junit4
+    implementation libs.android.support.appcompat
+
+    implementation project(':jme3-core')
+    implementation project(':jme3-android')
+    implementation project(':jme3-android-native')
+    implementation project(':jme3-effects')
+    implementation project(':jme3-jbullet')
+    implementation project(':jme3-networking')
+    implementation project(':jme3-niftygui')
+    implementation project(':jme3-plugins')
+    implementation project(':jme3-terrain')
+    implementation fileTree(dir: '../jme3-examples/build/libs', include: ['*.jar'], exclude: ['*sources*.*'])
 }

+ 3 - 3
jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java

@@ -38,7 +38,7 @@ public class TestAndroidSensors extends SimpleApplication implements ActionListe
 
     private Geometry geomZero = null;
     // Map of joysticks saved with the joyId as the key
-    private IntMap<Joystick> joystickMap = new IntMap<Joystick>();
+    private IntMap<Joystick> joystickMap = new IntMap<>();
     // flag to allow for the joystick axis to be calibrated on startup
     private boolean initialCalibrationComplete = false;
     // mappings used for onAnalog
@@ -228,7 +228,7 @@ public class TestAndroidSensors extends SimpleApplication implements ActionListe
         }
     }
 
-
+    @Override
     public void onAction(String string, boolean pressed, float tpf) {
        if (string.equalsIgnoreCase("MouseClick") && pressed) {
             // Calibrate the axis (set new zero position) if the axis
@@ -256,7 +256,7 @@ public class TestAndroidSensors extends SimpleApplication implements ActionListe
         }
     }
 
-
+    @Override
     public void onAnalog(String string, float value, float tpf) {
         logger.log(Level.INFO, "onAnalog for {0}, value: {1}, tpf: {2}",
                 new Object[]{string, value, tpf});

+ 7 - 7
jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/CustomArrayAdapter.java

@@ -23,11 +23,11 @@ public class CustomArrayAdapter extends ArrayAdapter<String> {
     private int selectedPosition = -1;
     /* Background Color of selected item */
     private int selectedBackgroundColor = 0xffff00;
-    /* Background Color of non selected item */
+    /* Background Color of non-selected items */
     private int nonselectedBackgroundColor = 0x000000;
     /* Background Drawable Resource ID of selected item */
     private int selectedBackgroundResource = 0;
-    /* Background Drawable Resource ID of non selected items */
+    /* Background Drawable Resource ID of non-selected items */
     private int nonselectedBackgroundResource = 0;
 
     /* Variables to support list filtering */
@@ -53,7 +53,7 @@ public class CustomArrayAdapter extends ArrayAdapter<String> {
         this.selectedBackgroundColor = selectedBackgroundColor;
     }
 
-    /** Setter for non selected background color */
+    /** Setter for non-selected background color */
     public void setNonSelectedBackgroundColor(int nonselectedBackgroundColor) {
         this.nonselectedBackgroundColor = nonselectedBackgroundColor;
     }
@@ -63,7 +63,7 @@ public class CustomArrayAdapter extends ArrayAdapter<String> {
         this.selectedBackgroundResource = selectedBackgroundResource;
     }
 
-    /** Setter for non selected background resource id*/
+    /** Setter for non-selected background resource id*/
     public void setNonSelectedBackgroundResource(int nonselectedBackgroundResource) {
         this.nonselectedBackgroundResource = nonselectedBackgroundResource;
     }
@@ -125,13 +125,13 @@ public class CustomArrayAdapter extends ArrayAdapter<String> {
             String prefix = constraint.toString().toLowerCase();
             Log.i(TAG, "performFiltering: entries size: " + entries.size());
             if (prefix == null || prefix.length() == 0){
-                ArrayList<String> list = new ArrayList<String>(entries);
+                ArrayList<String> list = new ArrayList<>(entries);
                 results.values = list;
                 results.count = list.size();
                 Log.i(TAG, "clearing filter with size: " + list.size());
             }else{
-                final ArrayList<String> list = new ArrayList<String>(entries);
-                final ArrayList<String> nlist = new ArrayList<String>();
+                final ArrayList<String> list = new ArrayList<>(entries);
+                final ArrayList<String> nlist = new ArrayList<>();
                 int count = list.size();
 
                 for (int i = 0; i<count; i++){

+ 4 - 4
jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java

@@ -74,8 +74,8 @@ public class MainActivity extends AppCompatActivity implements OnItemClickListen
     /* Fields to contain the current position and display contents of the spinner */
     private int currentPosition = 0;
     private String currentSelection = "";
-    private List<String> classNames = new ArrayList<String>();
-    private List<String> exclusions = new ArrayList<String>();
+    private List<String> classNames = new ArrayList<>();
+    private List<String> exclusions = new ArrayList<>();
     private String rootPackage;
 
     /* ListView that displays the test application class names. */
@@ -263,7 +263,7 @@ public class MainActivity extends AppCompatActivity implements OnItemClickListen
         boolean include = true;
         /* check to see if the class in inside the rootPackage package */
         if (className.startsWith(rootPackage)) {
-            /* check to see if the class contains any of the exlusion strings */
+            /* check to see if the class contains any of the exclusion strings */
             for (int i = 0; i < exclusions.size(); i++) {
                 if (className.contains(exclusions.get(i))) {
                     Log.d(TAG, "Skipping Class " + className + ". Includes exclusion string: " + exclusions.get(i) + ".");
@@ -364,7 +364,7 @@ public class MainActivity extends AppCompatActivity implements OnItemClickListen
         setSelection(-1);
     }
 
-    public void afterTextChanged(Editable edtbl) {
+    public void afterTextChanged(Editable editable) {
     }
 
     @Override

+ 62 - 0
jme3-android-native/bufferallocator.gradle

@@ -0,0 +1,62 @@
+// build file for native buffer allocator, created by pavl_g on 5/17/22.
+
+// directories for native source
+String bufferAllocatorAndroidPath = 'src/native/jme_bufferallocator'
+String bufferAllocatorHeaders = 'src/native/headers'
+
+//Pre-compiled libs directory
+def rootPath = rootProject.projectDir.absolutePath
+String bufferAllocatorPreCompiledLibsDir =
+        rootPath + File.separator + "build" + File.separator + 'native' + File.separator + 'android' + File.separator + 'allocator'
+
+// directories for build
+String bufferAllocatorBuildDir = "$buildDir" + File.separator + "bufferallocator"
+String bufferAllocatorJniDir = bufferAllocatorBuildDir + File.separator + "jni"
+String bufferAllocatorHeadersBuildDir = bufferAllocatorJniDir + File.separator + "headers"
+String bufferAllocatorBuildLibsDir = bufferAllocatorBuildDir + File.separator + "libs"
+
+// copy native src to build dir
+task copyJmeBufferAllocator(type: Copy) {
+    from file(bufferAllocatorAndroidPath)
+    into file(bufferAllocatorJniDir)
+}
+
+// copy native headers to build dir
+task copyJmeHeadersBufferAllocator(type: Copy, dependsOn: copyJmeBufferAllocator) {
+    from file(bufferAllocatorHeaders)
+    into file(bufferAllocatorHeadersBuildDir)
+}
+
+// compile and build copied natives in build dir
+task buildBufferAllocatorNativeLib(type: Exec, dependsOn: [copyJmeBufferAllocator, copyJmeHeadersBufferAllocator]) {
+    workingDir bufferAllocatorBuildDir
+    executable rootProject.ndkCommandPath
+    args "-j" + Runtime.runtime.availableProcessors()
+}
+
+task updatePreCompiledLibsBufferAllocator(type: Copy, dependsOn: buildBufferAllocatorNativeLib) {
+    from file(bufferAllocatorBuildLibsDir)
+    into file(bufferAllocatorPreCompiledLibsDir)
+}
+
+// Copy pre-compiled libs to build directory (when not building new libs)
+task copyPreCompiledLibsBufferAllocator(type: Copy) {
+    from file(bufferAllocatorPreCompiledLibsDir)
+    into file(bufferAllocatorBuildLibsDir)
+}
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledLibsBufferAllocator.dependsOn(rootProject.extractPrebuiltNatives)
+}
+
+// ndkExists is a boolean from the build.gradle in the root project
+// buildNativeProjects is a string set to "true"
+if (ndkExists && buildNativeProjects == "true") {
+    // build native libs and update stored pre-compiled libs to commit
+    compileJava.dependsOn { updatePreCompiledLibsBufferAllocator }
+} else {
+    // use pre-compiled native libs (not building new ones)
+    compileJava.dependsOn { copyPreCompiledLibsBufferAllocator }
+}
+
+// package the native object files inside the lib folder in a production jar
+jar.into("lib") { from bufferAllocatorBuildLibsDir }

+ 3 - 17
jme3-android-native/build.gradle

@@ -2,18 +2,6 @@
 //   for this project. This initialization is applied in the "build.gradle"
 //   of the root project.
 
-// NetBeans will automatically add "run" and "debug" tasks relying on the
-// "mainClass" property. You may however define the property prior executing
-// tasks by passing a "-PmainClass=<QUALIFIED_CLASS_NAME>" argument.
-//
-// Note however, that you may define your own "run" and "debug" task if you
-// prefer. In this case NetBeans will not add these tasks but you may rely on
-// your own implementation.
-
-if (!hasProperty('mainClass')) {
-    ext.mainClass = ''
-}
-
 sourceSets {
     main {
         java {
@@ -30,20 +18,18 @@ dependencies {
     //
     // You can read more about how to add dependency here:
     //   http://www.gradle.org/docs/current/userguide/dependency_management.html#sec:how_to_declare_your_dependencies
-    compile project(':jme3-android')
+    api project(':jme3-android')
 }
 
 ext {
     // stores the native project classpath to be used in each native
     // build to generate native header files
-    projectClassPath = configurations.runtime.asFileTree.matching {
+    projectClassPath = configurations.runtimeClasspath.asFileTree.matching {
         exclude ".gradle"
     }.asPath
 }
-//println "projectClassPath = " + projectClassPath
 
 // add each native lib build file
 apply from: file('openalsoft.gradle')
-// apply from: file('stb_image.gradle')
-// apply from: file('tremor.gradle')
 apply from: file('decode.gradle')
+apply from: file('bufferallocator.gradle')

+ 4 - 1
jme3-android-native/decode.gradle

@@ -83,6 +83,9 @@ task copyPreCompiledLibs(type: Copy) {
     from sourceDir
     into outputDir
 }
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledLibs.dependsOn(rootProject.extractPrebuiltNatives)
+}
 
 // ndkExists is a boolean from the build.gradle in the root project
 // buildNativeProjects is a string set to "true"
@@ -96,7 +99,7 @@ if (ndkExists && buildNativeProjects == "true") {
 
 jar.into("lib") { from decodeBuildLibsDir }
 
-// Helper class to wrap ant dowload task
+// Helper class to wrap ant download task
 class MyDownload extends DefaultTask {
     @Input
     String sourceUrl

+ 4 - 1
jme3-android-native/openalsoft.gradle

@@ -111,6 +111,9 @@ task copyPreCompiledOpenAlSoftLibs(type: Copy) {
     from sourceDir
     into outputDir
 }
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledOpenAlSoftLibs.dependsOn(rootProject.extractPrebuiltNatives)
+}
 
 // ndkExists is a boolean from the build.gradle in the root project
 // buildNativeProjects is a string set to "true"
@@ -124,7 +127,7 @@ if (ndkExists && buildNativeProjects == "true") {
 
 jar.into("lib") { from openalsoftBuildLibsDir }
 
-// Helper class to wrap ant dowload task
+// Helper class to wrap ant download task
 class MyDownload extends DefaultTask {
     @Input
     String sourceUrl

+ 50 - 0
jme3-android-native/src/native/jme_bufferallocator/Android.mk

@@ -0,0 +1,50 @@
+# 
+#  Copyright (c) 2009-2022 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.
+
+##
+# Created by pavl_g on 5/17/22.
+# For more : https://developer.android.com/ndk/guides/android_mk.
+##
+TARGET_PLATFORM := android-19
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_LDLIBS     := -llog -Wl,-s
+
+LOCAL_MODULE := bufferallocatorjme
+
+LOCAL_C_INCLUDES := $(LOCAL_PATH)
+
+LOCAL_SRC_FILES := com_jme3_util_AndroidNativeBufferAllocator.c
+
+include $(BUILD_SHARED_LIBRARY)

+ 39 - 0
jme3-android-native/src/native/jme_bufferallocator/Application.mk

@@ -0,0 +1,39 @@
+#
+#  Copyright (c) 2009-2022 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.
+
+##
+# Created by pavl_g on 5/17/22.
+# For more : https://developer.android.com/ndk/guides/application_mk.
+##
+APP_PLATFORM := android-19
+# change this to 'debug' to see android logs
+APP_OPTIM := release
+APP_ABI := all

+ 99 - 0
jme3-android-native/src/native/jme_bufferallocator/com_jme3_util_AndroidNativeBufferAllocator.c

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2009-2022 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.
+ */
+
+/**
+ * @file com_jme3_util_AndroidNativeBufferAllocator.c
+ * @author pavl_g.
+ * @brief Creates and releases direct byte buffers for {com.jme3.util.AndroidNativeBufferAllocator}.
+ * @date 2022-05-17.
+ * @note
+ * Find more at :
+ * - JNI Direct byte buffers : https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewDirectByteBuffer.
+ * - JNI Get Direct byte buffer : https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetDirectBufferAddress.
+ * - GNU Basic allocation : https://www.gnu.org/software/libc/manual/html_node/Basic-Allocation.html.
+ * - GNU Allocating Cleared Space : https://www.gnu.org/software/libc/manual/html_node/Allocating-Cleared-Space.html.
+ * - GNU No Memory error : https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html#index-ENOMEM.
+ * - GNU Freeing memory : https://www.gnu.org/software/libc/manual/html_node/Freeing-after-Malloc.html.
+ * - Android logging : https://developer.android.com/ndk/reference/group/logging.
+ * - Android logging example : https://github.com/android/ndk-samples/blob/7a8ff4c5529fce6ec4c5796efbe773f5d0e569cc/hello-libs/app/src/main/cpp/hello-libs.cpp#L25-L26.
+ */
+
+#include "headers/com_jme3_util_AndroidNativeBufferAllocator.h"
+#include <stdlib.h>
+#include <stdbool.h>
+#include <errno.h>
+
+#ifndef NDEBUG
+#include <android/log.h>
+#define LOG(LOG_ID, ...) __android_log_print(LOG_ID, \
+                    "AndroidNativeBufferAllocator", ##__VA_ARGS__);
+#else
+#define LOG(...)
+#endif
+
+bool isDeviceOutOfMemory(void*);
+
+/**
+ * @brief Tests if the device is out of memory.
+ *
+ * @return true if the buffer to allocate is a NULL pointer and the errno is ENOMEM (Error-no-memory).
+ * @return false otherwise.
+ */
+bool isDeviceOutOfMemory(void* buffer) {
+    return buffer == NULL && errno == ENOMEM;
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_util_AndroidNativeBufferAllocator_releaseDirectByteBuffer
+(JNIEnv * env, jobject object, jobject bufferObject)
+{
+    void* buffer = (*env)->GetDirectBufferAddress(env, bufferObject);
+    // deallocates the buffer pointer
+    free(buffer);
+    // log the destruction by mem address
+    LOG(ANDROID_LOG_INFO, "Buffer released (mem_address, size) -> (%p, %lu)", buffer, sizeof(buffer));
+    // avoid accessing this memory space by resetting the memory address
+    buffer = NULL;
+    LOG(ANDROID_LOG_INFO, "Buffer mem_address formatted (mem_address, size) -> (%p, %u)", buffer, sizeof(buffer));
+}
+
+JNIEXPORT jobject JNICALL Java_com_jme3_util_AndroidNativeBufferAllocator_createDirectByteBuffer
+(JNIEnv * env, jobject object, jlong size)
+{
+    void* buffer = calloc(1, size);
+    if (isDeviceOutOfMemory(buffer)) {
+       LOG(ANDROID_LOG_FATAL, "Device is out of memory exiting with %u", errno);
+       exit(errno);
+    } else {
+       LOG(ANDROID_LOG_INFO, "Buffer created successfully (mem_address, size) -> (%p %lli)", buffer, size);
+    }
+    return (*env)->NewDirectByteBuffer(env, buffer, size);
+}

+ 25 - 20
jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c

@@ -110,13 +110,18 @@ static int FileDesc_seek(void *datasource, ogg_int64_t offset, int whence)
     wrapper->current = actual_offset;
 }
 
-static int FileDesc_close(void *datasource)
+static int FileDesc_clear(void *datasource)
 {
     FileDescWrapper* wrapper = (FileDescWrapper*)datasource;
     
-    LOGI("FD close");
-    
-    return close(wrapper->fd);
+    LOGI("Clear resources -- delegating closure to the Android ParcelFileDescriptor");
+
+    /* release the file descriptor wrapper buffer */
+    free(wrapper);
+
+    wrapper = NULL;
+
+    return 0;
 }
 
 static long FileDesc_tell(void *datasource)
@@ -139,7 +144,7 @@ static long FileDesc_tell(void *datasource)
 static ov_callbacks FileDescCallbacks = {
     FileDesc_read,
     FileDesc_seek,
-    FileDesc_close,
+    FileDesc_clear,
     FileDesc_tell
 };
 
@@ -157,10 +162,10 @@ static jfieldID nvf_field_bitRate;
 static jfieldID nvf_field_totalBytes;
 static jfieldID nvf_field_duration;
 
-JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_nativeInit
+JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_preInit
   (JNIEnv *env, jclass clazz)
 {
-    LOGI("nativeInit");
+    LOGI("preInit");
     
     nvf_field_ovf = (*env)->GetFieldID(env, clazz, "ovf", "Ljava/nio/ByteBuffer;");;
     nvf_field_seekable = (*env)->GetFieldID(env, clazz, "seekable", "Z");
@@ -171,10 +176,10 @@ JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_nativeInit
     nvf_field_duration = (*env)->GetFieldID(env, clazz, "duration", "F");
 }
 
-JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_open
+JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_init
   (JNIEnv *env, jobject nvf, jint fd, jlong off, jlong len)
 {
-    LOGI("open: fd = %d, off = %lld, len = %lld", fd, off, len);
+    LOGI("init: fd = %d, off = %lld, len = %lld", fd, off, len);
     
     OggVorbis_File* ovf = (OggVorbis_File*) malloc(sizeof(OggVorbis_File));
     
@@ -189,19 +194,19 @@ JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_open
     
     if (result != 0)
     {
-        LOGI("ov_open fail");
+        LOGI("init fail");
         
         free(ovf);
         free(wrapper);
     
         char err[512];
-        sprintf(err, "ov_open failed: %d", result);
+        sprintf(err, "init failed: %d", result);
         throwIOException(env, err);
         
         return;
     }
     
-    LOGI("ov_open OK");
+    LOGI("init OK");
     jobject ovfBuf = (*env)->NewDirectByteBuffer(env, ovf, sizeof(OggVorbis_File));
     
     vorbis_info* info = ov_info(ovf, -1);
@@ -246,7 +251,7 @@ JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_seekTime
     }
 }
 
-JNIEXPORT jint JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_read
+JNIEXPORT jint JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_readIntoArray
   (JNIEnv *env, jobject nvf, jbyteArray buf, jint off, jint len)
 {
     int bitstream = -1;
@@ -288,7 +293,7 @@ JNIEXPORT jint JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_read
     return result;
 }
 
-JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_readFully
+JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_readIntoBuffer
   (JNIEnv *env, jobject nvf, jobject buf)
 {
     int bitstream = -1;
@@ -330,19 +335,19 @@ JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_readFully
     }
 }
 
-JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_close
+JNIEXPORT void JNICALL Java_com_jme3_audio_plugins_NativeVorbisFile_clearResources
   (JNIEnv *env, jobject nvf)
 {
-    LOGI("close");
+    LOGI("clearResources");
     
     jobject ovfBuf = (*env)->GetObjectField(env, nvf, nvf_field_ovf);
     OggVorbis_File* ovf = (OggVorbis_File*) (*env)->GetDirectBufferAddress(env, ovfBuf);
-    FileDescWrapper* wrapper = (FileDescWrapper*) ovf->datasource;
-    wrapper->env = env;
     
+    /* release the ovf resources */
     ov_clear(ovf);
-    
-    free(wrapper);
+    /* release the ovf buffer */
     free(ovf);
+    ovf = NULL;
+    /* destroy the java reference object */
     (*env)->SetObjectField(env, nvf, nvf_field_ovf, NULL);
 }

+ 4 - 6
jme3-android/build.gradle

@@ -1,12 +1,10 @@
 apply plugin: 'java'
 
-if (!hasProperty('mainClass')) {
-    ext.mainClass = ''
-}
-
 dependencies {
-    compile project(':jme3-core')
-    compile project(':jme3-plugins')
+    //added annotations used by JmeSurfaceView.
+    compileOnly libs.androidx.annotation
+    compileOnly libs.androidx.lifecycle.common
+    api project(':jme3-core')
     compileOnly 'android:android'
 }
 

+ 12 - 6
jme3-android/src/main/java/com/jme3/app/AndroidHarness.java

@@ -75,8 +75,8 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
     protected int eglDepthBits = 16;
 
     /**
-     * Sets the number of samples to use for multisampling.</br>
-     * Leave 0 (default) to disable multisampling.</br>
+     * Sets the number of samples to use for multisampling.<br>
+     * Leave 0 (default) to disable multisampling.<br>
      * Set to 2 or 4 to enable multisampling.
      */
     protected int eglSamples = 0;
@@ -190,6 +190,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public void onCreate(Bundle savedInstanceState) {
         initializeLogHandler();
 
@@ -211,7 +212,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
             logger.log(Level.FINE, "Using Retained App");
             this.app = data.app;
         } else {
-            // Discover the screen reolution
+            // Discover the screen resolution
             //TODO try to find a better way to get a hand on the resolution
             WindowManager wind = this.getWindowManager();
             Display disp = wind.getDefaultDisplay();
@@ -240,7 +241,7 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
             try {
                 if (app == null) {
                     Class clazz = Class.forName(appClass);
-                    app = (LegacyApplication)clazz.newInstance();
+                    app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
                 }
 
                 app.setSettings(settings);
@@ -358,8 +359,8 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
      * Called by the android alert dialog, terminate the activity and OpenGL
      * rendering
      *
-     * @param dialog
-     * @param whichButton
+     * @param dialog ignored
+     * @param whichButton the button index
      */
     @Override
     public void onClick(DialogInterface dialog, int whichButton) {
@@ -494,6 +495,11 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
         app.reshape(width, height);
     }
 
+    @Override
+    public void rescale(float x, float y) {
+        app.rescale(x, y);
+    }
+
     @Override
     public void update() {
         app.update();

+ 28 - 18
jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -103,8 +103,8 @@ public class AndroidHarnessFragment extends Fragment implements
     protected int eglDepthBits = 16;
 
     /**
-     * Sets the number of samples to use for multisampling.</br>
-     * Leave 0 (default) to disable multisampling.</br>
+     * Sets the number of samples to use for multisampling.<br>
+     * Leave 0 (default) to disable multisampling.<br>
      * Set to 2 or 4 to enable multisampling.
      */
     protected int eglSamples = 0;
@@ -128,9 +128,9 @@ public class AndroidHarnessFragment extends Fragment implements
      * If the surfaceview is rectangular, the longest side (width or height)
      * will have the resolution set to a maximum of maxResolutionDimension.
      * The other direction will be set to a value that maintains the aspect
-     * ratio of the surfaceview. </br>
+     * ratio of the surfaceview. <br>
      * Any value less than 0 (default = -1) will result in the surfaceview having the
-     * same resolution as the view layout (ie. no max resolution).
+     * same resolution as the view layout (i.e. no max resolution).
      */
     protected int maxResolutionDimension = -1;
 
@@ -229,9 +229,10 @@ public class AndroidHarnessFragment extends Fragment implements
      * other methods.  View related objects should not be reused, but rather
      * created and destroyed along with the Activity.
      *
-     * @param savedInstanceState
+     * @param savedInstanceState the saved instance state
      */
     @Override
+    @SuppressWarnings("unchecked")
     public void onCreate(Bundle savedInstanceState) {
         initializeLogHandler();
         logger.fine("onCreate");
@@ -258,7 +259,7 @@ public class AndroidHarnessFragment extends Fragment implements
         try {
             if (app == null) {
                 Class clazz = Class.forName(appClass);
-                app = (LegacyApplication)clazz.newInstance();
+                app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
             }
 
             app.setSettings(settings);
@@ -282,9 +283,9 @@ public class AndroidHarnessFragment extends Fragment implements
      * by the Activity's layout parameters for this Fragment.  For jME, we also
      * update the application reference to the new view.
      *
-     * @param inflater
-     * @param container
-     * @param savedInstanceState
+     * @param inflater ignored
+     * @param container ignored
+     * @param savedInstanceState ignored
      * @return the new view
      */
     @Override
@@ -312,7 +313,7 @@ public class AndroidHarnessFragment extends Fragment implements
     }
 
     /**
-     * When the Fragment resumes (ie. after app resumes or device screen turned
+     * When the Fragment resumes (i.e. after app resumes or device screen turned
      * back on), call the gainFocus() in the jME application.
      */
     @Override
@@ -324,7 +325,7 @@ public class AndroidHarnessFragment extends Fragment implements
     }
 
     /**
-     * When the Fragment pauses (ie. after home button pressed on the device
+     * When the Fragment pauses (i.e. after home button pressed on the device
      * or device screen turned off) , call the loseFocus() in the jME application.
      */
     @Override
@@ -431,8 +432,8 @@ public class AndroidHarnessFragment extends Fragment implements
      * Called by the android alert dialog, terminate the activity and OpenGL
      * rendering
      *
-     * @param dialog
-     * @param whichButton
+     * @param dialog ignored
+     * @param whichButton the button index
      */
     @Override
     public void onClick(DialogInterface dialog, int whichButton) {
@@ -571,6 +572,11 @@ public class AndroidHarnessFragment extends Fragment implements
         app.reshape(width, height);
     }
 
+    @Override
+    public void rescale(float x, float y) {
+        app.rescale(x, y);
+    }
+
     @Override
     public void update() {
         app.update();
@@ -673,8 +679,10 @@ public class AndroidHarnessFragment extends Fragment implements
                 int newHeight = bottom-top;
 
                 if (viewWidth != newWidth || viewHeight != newHeight) {
-                    logger.log(Level.FINE, "SurfaceView layout changed: old width: {0}, old height: {1}, new width: {2}, new height: {3}",
-                            new Object[]{viewWidth, viewHeight, newWidth, newHeight});
+                    if (logger.isLoggable(Level.FINE)) {
+                        logger.log(Level.FINE, "SurfaceView layout changed: old width: {0}, old height: {1}, new width: {2}, new height: {3}",
+                                new Object[]{viewWidth, viewHeight, newWidth, newHeight});
+                    }
                     viewWidth = newWidth;
                     viewHeight = newHeight;
 
@@ -694,8 +702,10 @@ public class AndroidHarnessFragment extends Fragment implements
                     }
                     // set the surfaceview resolution if the size != current view size
                     if (fixedSizeWidth != viewWidth || fixedSizeHeight != viewHeight) {
-                        logger.log(Level.FINE, "setting surfaceview resolution to width: {0}, height: {1}",
-                                new Object[]{fixedSizeWidth, fixedSizeHeight});
+                        if (logger.isLoggable(Level.FINE)) {
+                            logger.log(Level.FINE, "setting surfaceview resolution to width: {0}, height: {1}",
+                                    new Object[]{fixedSizeWidth, fixedSizeHeight});
+                        }
                         view.getHolder().setFixedSize(fixedSizeWidth, fixedSizeHeight);
                     }
                 }

+ 8 - 6
jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -126,10 +126,12 @@ public class MjpegFileWriter {
         int fileSize = (int)aviFile.length();
         logger.log(Level.INFO, "fileSize: {0}", fileSize);
         int listSize = (int) (fileSize - 8 - aviMovieOffset - indexlistBytes.length);
-        logger.log(Level.INFO, "listSize: {0}", listSize);
-        logger.log(Level.INFO, "aviFile canWrite: {0}", aviFile.canWrite());
-        logger.log(Level.INFO, "aviFile AbsolutePath: {0}", aviFile.getAbsolutePath());
-        logger.log(Level.INFO, "aviFile numFrames: {0}", numFrames);
+        if (logger.isLoggable(Level.INFO)) {
+            logger.log(Level.INFO, "listSize: {0}", listSize);
+            logger.log(Level.INFO, "aviFile canWrite: {0}", aviFile.canWrite());
+            logger.log(Level.INFO, "aviFile AbsolutePath: {0}", aviFile.getAbsolutePath());
+            logger.log(Level.INFO, "aviFile numFrames: {0}", numFrames);
+        }
 
         RandomAccessFile raf = new RandomAccessFile(aviFile, "rw");
 
@@ -457,7 +459,7 @@ public class MjpegFileWriter {
 
         public byte[] fcc = new byte[]{'i', 'd', 'x', '1'};
         public int cb = 0;
-        public List<AVIIndex> ind = new ArrayList<AVIIndex>();
+        public List<AVIIndex> ind = new ArrayList<>();
 
         public AVIIndexList() {
         }

+ 15 - 11
jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -55,7 +55,7 @@ import java.util.logging.Logger;
 
 /**
  * A Video recording AppState that records the screen output into an AVI file with
- * M-JPEG content. The file should be playable on any OS in any video player.<br/>
+ * M-JPEG content. The file should be playable on any OS in any video player.<br>
  * The video recording starts when the state is attached and stops when it is detached
  * or the application is quit. You can set the fileName of the file to be written when the
  * state is detached, else the old file will be overwritten. If you specify no file
@@ -132,9 +132,14 @@ public class VideoRecorderAppState extends AbstractAppState {
     }
 
     /**
-     * This constructor allows you to specify the output file of the video as well as the quality
+     * This constructor allows you to specify the output file of the video as
+     * well as the quality.
+     *
      * @param file the video file
-     * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
+     * @param quality the quality of the jpegs in the video stream (0.0 smallest
+     * file - 1.0 largest file)
+     * @param framerate the frame rate of the resulting video, the application
+     * will be locked to this framerate
      */
     public VideoRecorderAppState(File file, float quality, int framerate) {
         this.file = file;
@@ -222,12 +227,11 @@ public class VideoRecorderAppState extends AbstractAppState {
         private int width;
         private int height;
         private RenderManager renderManager;
-        private boolean isInitilized = false;
+        private boolean isInitialized = false;
         private LinkedBlockingQueue<WorkItem> freeItems;
-        private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<WorkItem>();
+        private LinkedBlockingQueue<WorkItem> usedItems = new LinkedBlockingQueue<>();
         private MjpegFileWriter writer;
         private boolean fastMode = true;
-        private AppProfiler prof;
 
         public void addImage(Renderer renderer, FrameBuffer out) {
             if (freeItems == null) {
@@ -269,7 +273,7 @@ public class VideoRecorderAppState extends AbstractAppState {
             this.width = camera.getWidth();
             this.height = camera.getHeight();
             this.renderManager = rm;
-            this.isInitilized = true;
+            this.isInitialized = true;
             if (freeItems == null) {
                 freeItems = new LinkedBlockingQueue<WorkItem>();
                 for (int i = 0; i < numCpus; i++) {
@@ -284,7 +288,7 @@ public class VideoRecorderAppState extends AbstractAppState {
 
         @Override
         public boolean isInitialized() {
-            return this.isInitilized;
+            return this.isInitialized;
         }
 
         @Override
@@ -326,7 +330,7 @@ public class VideoRecorderAppState extends AbstractAppState {
 
         @Override
         public void setProfiler(AppProfiler profiler) {
-            this.prof = profiler;
+            // not implemented
         }
     }
 
@@ -371,7 +375,7 @@ public class VideoRecorderAppState extends AbstractAppState {
                     Thread.sleep(difference);
                 } catch (InterruptedException ex) {
                 }
-            } else {
+            } else if (logger.isLoggable(Level.INFO)) {
                 logger.log(Level.INFO, "actual tpf(ms): {0}, 1/framerate(ms): {1}",
                         new Object[]{difference, (1.0f / this.framerate) * 1000.0f});
             }

+ 2 - 1
jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java

@@ -26,11 +26,12 @@ public class AndroidLocator implements AssetLocator {
     public AssetInfo locate(AssetManager manager, AssetKey key) {
         String assetPath = rootPath + key.getName();
         // Fix path issues
+        assetPath = assetPath.replace("//", "/");
         if (assetPath.startsWith("/")) {
             // Remove leading /
             assetPath = assetPath.substring(1);
         }
-        assetPath = assetPath.replace("//", "/");
+        
 
         // Not making this a property and storing for future use in case the view stored in JmeAndroidSystem
         // is replaced due to device orientation change.  Not sure it is necessary to do this yet, but am for now.

+ 35 - 0
jme3-android/src/main/java/com/jme3/audio/android/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+/**
+ * audio support for Android devices
+ */
+package com.jme3.audio.android;

+ 93 - 9
jme3-android/src/main/java/com/jme3/audio/plugins/NativeVorbisFile.java

@@ -1,8 +1,46 @@
+/*
+ * Copyright (c) 2009-2023 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.audio.plugins;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
+/**
+ * Represents the android implementation for the native <a href="https://xiph.org"> vorbis file decoder.</a>
+ * This decoder initializes an OggVorbis_File from an already opened file designated by the {@link NativeVorbisFile#fd}.
+ * 
+ * @author Kirill Vainer
+ * @author Modified by pavl_g
+ */
 public class NativeVorbisFile {
     
     public int fd;
@@ -16,22 +54,68 @@ public class NativeVorbisFile {
     
     static {
         System.loadLibrary("decodejme");
-        nativeInit();
+        preInit();
     }
     
-    public NativeVorbisFile(int fd, long off, long len) throws IOException {
-        open(fd, off, len);
+    /**
+     * Initializes an ogg vorbis native file from a file descriptor [fd] of an already opened file.
+     * 
+     * @param fd an integer representing the file descriptor 
+     * @param offset an integer indicating the start of the buffer
+     * @param length an integer indicating the end of the buffer
+     * @throws IOException in cases of a failure to initialize the vorbis file 
+     */
+    public NativeVorbisFile(int fd, long offset, long length) throws IOException {
+        init(fd, offset, length);
     }
     
-    private native void open(int fd, long off, long len) throws IOException;
-    
+    /**
+     * Seeks to a playback time relative to the decompressed pcm (Pulse-code modulation) stream.
+     * 
+     * @param time the playback seek time
+     * @throws IOException if the seek is not successful
+     */
     public native void seekTime(double time) throws IOException;
     
-    public native int read(byte[] buf, int off, int len) throws IOException;
+    /**
+     * Reads the vorbis file into a primitive byte buffer [buf] with an [offset] indicating the start byte and a [length] indicating the end byte on the output buffer.
+     * 
+     * @param buffer a primitive byte buffer to read the data into it
+     * @param offset an integer representing the offset or the start byte on the output buffer
+     * @param length an integer representing the end byte on the output buffer
+     * @return the number of the read bytes, (-1) if the reading has failed indicating an EOF, 
+     *         returns (0) if the reading has failed or the primitive [buffer] passed is null
+     * @throws IOException if the library has failed to read the file into the [out] buffer
+     *                     or if the java primitive byte array [buffer] is inaccessible
+     */
+    public native int readIntoArray(byte[] buffer, int offset, int length) throws IOException;
+    
+    /**
+     * Reads the vorbis file into a direct {@link java.nio.ByteBuffer}, starting from offset [0] till the buffer end on the output buffer.
+     * 
+     * @param out a reference to the output direct buffer 
+     * @throws IOException if a premature EOF is encountered before reaching the end of the buffer
+     *                     or if the library has failed to read the file into the [out] buffer
+     */
+    public native void readIntoBuffer(ByteBuffer out) throws IOException;
     
-    public native void readFully(ByteBuffer out) throws IOException;
+    /**
+     * Clears the native resources and destroys the buffer {@link NativeVorbisFile#ovf} reference.
+     */
+    public native void clearResources();
     
-    public native void close();
+    /**
+     * Prepares the java fields for the native environment.
+     */
+    private static native void preInit();
     
-    public static native void nativeInit();
+    /**
+     * Initializes an ogg vorbis native file from a file descriptor [fd] of an already opened file.
+     * 
+     * @param fd an integer representing the file descriptor 
+     * @param offset an integer representing the start of the buffer
+     * @param length an integer representing the length of the buffer
+     * @throws IOException in cases of a failure to initialize the vorbis file 
+     */
+    private native void init(int fd, long offset, long length) throws IOException;
 }

+ 37 - 6
jme3-android/src/main/java/com/jme3/audio/plugins/NativeVorbisLoader.java

@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2009-2023 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.audio.plugins;
 
 import android.content.res.AssetFileDescriptor;
@@ -33,12 +64,12 @@ public class NativeVorbisLoader implements AssetLoader {
         
         @Override
         public int read(byte[] buf) throws IOException {
-            return file.read(buf, 0, buf.length);
+            return file.readIntoArray(buf, 0, buf.length);
         }
         
         @Override
         public int read(byte[] buf, int off, int len) throws IOException {
-            return file.read(buf, off, len);
+            return file.readIntoArray(buf, off, len);
         }
         
         @Override
@@ -57,7 +88,7 @@ public class NativeVorbisLoader implements AssetLoader {
         
         @Override
         public void close() throws IOException {
-            file.close();
+            file.clearResources();
             afd.close();
         }
     }
@@ -71,14 +102,14 @@ public class NativeVorbisLoader implements AssetLoader {
             int fd = afd.getParcelFileDescriptor().getFd();
             file = new NativeVorbisFile(fd, afd.getStartOffset(), afd.getLength());
             ByteBuffer data = BufferUtils.createByteBuffer(file.totalBytes);
-            file.readFully(data);
+            file.readIntoBuffer(data);
             AudioBuffer ab = new AudioBuffer();
             ab.setupFormat(file.channels, 16, file.sampleRate);
             ab.updateData(data);
             return ab;
         } finally {
             if (file != null) {
-                file.close();
+                file.clearResources();
             }
             if (afd != null) {
                 afd.close();
@@ -107,7 +138,7 @@ public class NativeVorbisLoader implements AssetLoader {
         } finally {
             if (!success) {
                 if (file != null) {
-                    file.close();
+                    file.clearResources();
                 }
                 if (afd != null) {
                     afd.close();

+ 4 - 4
jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -66,7 +66,7 @@ public class AndroidGestureProcessor implements
 
     @Override
     public boolean onDown(MotionEvent event) {
-        // start of all GestureListeners.  Not really a gesture by itself
+        // The 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.
@@ -116,11 +116,11 @@ public class AndroidGestureProcessor implements
 
     @Override
     public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) {
-        // if not scaleInProgess, send scroll events.  This is to avoid sending
+        // if not scaleInProgress, 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.
+        // Apparently, 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 (!touchInput.getScaleDetector().isInProgress()) {

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -47,7 +47,7 @@ import java.util.logging.Logger;
 /**
  * <code>AndroidInput</code> is the main class that connects the Android system
  * 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>
+ * 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

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -41,7 +41,7 @@ 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>
+ * 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.
  *

+ 9 - 7
jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -51,15 +51,15 @@ import java.util.logging.Logger;
 /**
  * Main class that manages various joystick devices.  Joysticks can be many forms
  * including a simulated joystick to communicate the device orientation as well
- * as physical gamepads. </br>
+ * as physical gamepads. <br>
  * This class manages all the joysticks and feeds the inputs from each back
  * to jME's InputManager.
  *
  * This handler also supports the joystick.rumble(rumbleAmount) method.  In this
  * case, when joystick.rumble(rumbleAmount) is called, the Android device will vibrate
- * if the device has a built in vibrate motor.
+ * if the device has a built-in vibrate motor.
  *
- * Because Andorid does not allow for the user to define the intensity of the
+ * Because Android does not allow for the user to define the intensity of the
  * vibration, the rumble amount (ie strength) is converted into vibration pulses
  * The stronger the strength amount, the shorter the delay between pulses.  If
  * amount is 1, then the vibration stays on the whole time.  If amount is 0.5,
@@ -83,14 +83,14 @@ public class AndroidJoyInput implements JoyInput {
     public static boolean disableSensors = false;
 
     protected AndroidInputHandler inputHandler;
-    protected List<Joystick> joystickList = new ArrayList<Joystick>();
+    protected List<Joystick> joystickList = new ArrayList<>();
 //    private boolean dontSendHistory = false;
 
 
     // Internal
     private boolean initialized = false;
     private RawInputListener listener = null;
-    private ConcurrentLinkedQueue<InputEvent> eventQueue = new ConcurrentLinkedQueue<InputEvent>();
+    private ConcurrentLinkedQueue<InputEvent> eventQueue = new ConcurrentLinkedQueue<>();
     private AndroidSensorJoyInput sensorJoyInput;
     private Vibrator vibrator = null;
     private boolean vibratorActive = false;
@@ -209,7 +209,9 @@ public class AndroidJoyInput implements JoyInput {
 
     @Override
     public Joystick[] loadJoysticks(InputManager inputManager) {
-        logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName());
+        if (logger.isLoggable(Level.INFO)) {
+            logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName());
+        }
         if (!disableSensors) {
             joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager));
         }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -39,7 +39,7 @@ import java.util.logging.Logger;
 
 /**
  * <code>AndroidJoyInput14</code> extends <code>AndroidJoyInput</code>
- * to include support for physical joysticks/gamepads.</br>
+ * to include support for physical joysticks/gamepads.
  *
  * @author iwgeric
  */

+ 31 - 23
jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -51,6 +51,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -63,9 +64,8 @@ import java.util.logging.Logger;
 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 Map<Integer, AndroidJoystick> joystickIndex = new HashMap<>();
 
     private static int[] AndroidGamepadButtons = {
             // Dpad buttons
@@ -109,7 +109,7 @@ public class AndroidJoystickJoyInput14 {
 
     public List<Joystick> loadJoysticks(int joyId, InputManager inputManager) {
         logger.log(Level.INFO, "loading Joystick devices");
-        ArrayList<Joystick> joysticks = new ArrayList<Joystick>();
+        ArrayList<Joystick> joysticks = new ArrayList<>();
         joysticks.clear();
         joystickIndex.clear();
 
@@ -118,7 +118,9 @@ public class AndroidJoystickJoyInput14 {
         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});
+            if (logger.isLoggable(Level.FINE)) {
+                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) ||
@@ -127,9 +129,9 @@ public class AndroidJoystickJoyInput14 {
                 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
+                    // Create an AndroidJoystick and store the InputDevice, so we
+                    // can later convert the input from the InputDevice to the
+                    // appropriate jME Joystick event.
                     AndroidJoystick joystick = new AndroidJoystick(inputManager,
                                                                 joyInput,
                                                                 dev,
@@ -144,10 +146,14 @@ public class AndroidJoystickJoyInput14 {
                     // 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());
+                        if (logger.isLoggable(Level.INFO)) {
+                            logger.log(Level.INFO, "motion range: {0}", motionRange);
+                            logger.log(Level.INFO, "axis: {0}", motionRange.getAxis());
+                        }
                         JoystickAxis axis = joystick.addAxis(motionRange);
-                        logger.log(Level.INFO, "added axis: {0}", axis);
+                        if (logger.isLoggable(Level.INFO)) {
+                            logger.log(Level.INFO, "added axis: {0}", axis);
+                        }
                     }
 
                     // InputDevice has a method for determining if a keyCode is
@@ -158,8 +164,10 @@ public class AndroidJoystickJoyInput14 {
                     // 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 (logger.isLoggable(Level.INFO)) {
+                            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
@@ -173,13 +181,12 @@ public class AndroidJoystickJoyInput14 {
             }
         }
 
-
-        loaded = true;
         return joysticks;
     }
 
     public boolean onGenericMotion(MotionEvent event) {
         boolean consumed = false;
+        float rawValue, value;
 //        logger.log(Level.INFO, "onGenericMotion event: {0}", event);
         event.getDeviceId();
         event.getSource();
@@ -188,7 +195,8 @@ public class AndroidJoystickJoyInput14 {
         if (joystick != null) {
             for (int androidAxis: joystick.getAndroidAxes()) {
                 String axisName = MotionEvent.axisToString(androidAxis);
-                float value = event.getAxisValue(androidAxis);
+                rawValue = event.getAxisValue(androidAxis);
+                value = JoystickCompatibilityMappings.remapAxisRange(joystick.getAxis(androidAxis), rawValue);
                 int action = event.getAction();
                 if (action == MotionEvent.ACTION_MOVE) {
 //                    logger.log(Level.INFO, "MOVE axis num: {0}, axisName: {1}, value: {2}",
@@ -197,7 +205,7 @@ public class AndroidJoystickJoyInput14 {
                     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);
+                        JoyAxisEvent axisEvent = new JoyAxisEvent(axis, value, rawValue);
                         joyInput.addEvent(axisEvent);
                         consumed = true;
                     } else {
@@ -245,8 +253,8 @@ public class AndroidJoystickJoyInput14 {
         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>();
+        private Map<Integer, JoystickAxis> axisIndex = new HashMap<>();
+        private Map<Integer, JoystickButton> buttonIndex = new HashMap<>();
 
         public AndroidJoystick( InputManager inputManager, JoyInput joyInput, InputDevice device,
                                int joyId, String name ) {
@@ -319,8 +327,8 @@ public class AndroidJoystickJoyInput14 {
                 original = JoystickButton.BUTTON_11;
             }
 
-            String logicalId = JoystickCompatibilityMappings.remapComponent( getName(), original );
-            if( logicalId == null ? original != null : !logicalId.equals(original) ) {
+            String logicalId = JoystickCompatibilityMappings.remapButton( getName(), original );
+            if (logger.isLoggable(Level.FINE) && !Objects.equals(logicalId, original)) {
                 logger.log(Level.FINE, "Remapped: {0} to: {1}",
                         new Object[]{original, logicalId});
             }
@@ -350,8 +358,8 @@ public class AndroidJoystickJoyInput14 {
             } 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) ) {
+            String logicalId = JoystickCompatibilityMappings.remapAxis( getName(), original );
+            if (logger.isLoggable(Level.FINE) && !Objects.equals(logicalId, original)) {
                 logger.log(Level.FINE, "Remapped: {0} to: {1}",
                         new Object[]{original, logicalId});
             }

+ 7 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidKeyMapping.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -143,6 +143,12 @@ public class AndroidKeyMapping {
         0x0,//mute
     };
 
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private AndroidKeyMapping() {
+    }
+
     public static int getJmeKey(int androidKey) {
         if (androidKey > ANDROID_TO_JME.length) {
             return androidKey;

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -78,7 +78,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
     private AndroidJoyInput joyInput;
     private SensorManager sensorManager = null;
     private WindowManager windowManager = null;
-    private IntMap<SensorData> sensors = new IntMap<SensorData>();
+    private IntMap<SensorData> sensors = new IntMap<>();
     private int lastRotation = 0;
     private boolean loaded = false;
 
@@ -96,7 +96,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         int sensorAccuracy = -1;
         float[] lastValues;
         final Object valuesLock = new Object();
-        ArrayList<AndroidSensorJoystickAxis> axes = new ArrayList<AndroidSensorJoystickAxis>();
+        ArrayList<AndroidSensorJoystickAxis> axes = new ArrayList<>();
         boolean enabled = false;
         boolean haveData = false;
 
@@ -155,16 +155,20 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         SensorData sensorData = sensors.get(sensorType);
         if (sensorData != null) {
             if (sensorData.enabled) {
-                logger.log(Level.FINE, "Sensor Already Active: SensorType: {0}, active: {1}",
-                        new Object[]{sensorType, sensorData.enabled});
+                if (logger.isLoggable(Level.FINE)) {
+                    logger.log(Level.FINE, "Sensor Already Active: SensorType: {0}, active: {1}",
+                            new Object[]{sensorType, sensorData.enabled});
+                }
                 return true;
             }
             sensorData.haveData = false;
             if (sensorData.sensor != null) {
                 if (sensorManager.registerListener(this, sensorData.sensor, sensorData.androidSensorSpeed)) {
                     sensorData.enabled = true;
-                    logger.log(Level.FINE, "SensorType: {0}, actived: {1}",
-                            new Object[]{sensorType, sensorData.enabled});
+                    if (logger.isLoggable(Level.FINE)) {
+                        logger.log(Level.FINE, "SensorType: {0}, enabled: {1}",
+                                new Object[]{sensorType, sensorData.enabled});
+                    }
                     return true;
                 } else {
                     sensorData.enabled = false;
@@ -183,8 +187,10 @@ public class AndroidSensorJoyInput implements SensorEventListener {
             }
             sensorData.enabled = false;
             sensorData.haveData = false;
-            logger.log(Level.FINE, "SensorType: {0} deactivated, active: {1}",
-                    new Object[]{sensorType, sensorData.enabled});
+            if (logger.isLoggable(Level.FINE)) {
+                logger.log(Level.FINE, "SensorType: {0} deactivated, active: {1}",
+                        new Object[]{sensorType, sensorData.enabled});
+            }
         }
     }
 
@@ -276,7 +282,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
      * Surface.ROTATION_270 = device in rotated 270deg counterclockwise
      *
      * When the Manifest locks the orientation, this value will not change during
-     * gametime, but if the orientation of the screen is based off the sensor,
+     * game time, but if the orientation of the screen is based off the sensor,
      * this value will change as the device is rotated.
      * @return Current device rotation amount
      */
@@ -374,7 +380,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                                 sensorData.haveData = true;
                             } else {
                                 if (axis.isChanged()) {
-                                    joyInput.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
+                                    joyInput.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue(), axis.getJoystickAxisValue()));
                                 }
                             }
                         }
@@ -409,9 +415,11 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                                     "AndroidSensorsJoystick");
 
         List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
-        for (Sensor sensor: availSensors) {
-            logger.log(Level.FINE, "{0} Sensor is available, Type: {1}, Vendor: {2}, Version: {3}",
-                    new Object[]{sensor.getName(), sensor.getType(), sensor.getVendor(), sensor.getVersion()});
+        if (logger.isLoggable(Level.FINE)) {
+            for (Sensor sensor : availSensors) {
+                logger.log(Level.FINE, "{0} Sensor is available, Type: {1}, Vendor: {2}, Version: {3}",
+                        new Object[]{sensor.getName(), sensor.getType(), sensor.getVendor(), sensor.getVersion()});
+            }
         }
 
         // manually create orientation sensor data since orientation is not a physical sensor
@@ -553,7 +561,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
                             sensorData.haveData = true;
                         } else {
                             if (axis.isChanged()) {
-                                JoyAxisEvent event = new JoyAxisEvent(axis, axis.getJoystickAxisValue());
+                                JoyAxisEvent event = new JoyAxisEvent(axis, axis.getJoystickAxisValue(), axis.getJoystickAxisValue());
 //                                logger.log(Level.INFO, "adding JoyAxisEvent: {0}", event);
                                 joyInput.addEvent(event);
 //                                joyHandler.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
@@ -575,10 +583,12 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         int sensorType = sensor.getType();
         SensorData sensorData = sensors.get(sensorType);
         if (sensorData != null) {
-            logger.log(Level.FINE, "onAccuracyChanged for {0}: accuracy: {1}",
-                    new Object[]{sensor.getName(), i});
-            logger.log(Level.FINE, "MaxRange: {0}, Resolution: {1}",
-                    new Object[]{sensor.getMaximumRange(), sensor.getResolution()});
+            if (logger.isLoggable(Level.FINE)) {
+                logger.log(Level.FINE, "onAccuracyChanged for {0}: accuracy: {1}",
+                        new Object[]{sensor.getName(), i});
+                logger.log(Level.FINE, "MaxRange: {0}, Resolution: {1}",
+                        new Object[]{sensor.getMaximumRange(), sensor.getResolution()});
+            }
             sensorData.sensorAccuracy = i;
         }
     }
@@ -705,8 +715,10 @@ public class AndroidSensorJoyInput implements SensorEventListener {
         @Override
         public void calibrateCenter() {
             zeroRawValue = lastRawValue;
-            logger.log(Level.FINE, "Calibrating axis {0} to {1}",
-                    new Object[]{getName(), zeroRawValue});
+            if (logger.isLoggable(Level.FINE)) {
+                logger.log(Level.FINE, "Calibrating axis {0} to {1}",
+                        new Object[]{getName(), zeroRawValue});
+            }
         }
 
     }

+ 13 - 12
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -57,7 +57,7 @@ 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
+ * extended by other classes to add features that were introduced after
  * Android rev 9.
  *
  * @author iwgeric
@@ -69,11 +69,10 @@ public class AndroidTouchInput implements TouchInput {
     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>();
+    final private HashMap<Integer, Vector2f> lastPositions = new HashMap<>();
+    final private ConcurrentLinkedQueue<InputEvent> inputEventQueue = new ConcurrentLinkedQueue<>();
     private final static int MAX_TOUCH_EVENTS = 1024;
     private final TouchEventPool touchEventPool = new TouchEventPool(MAX_TOUCH_EVENTS);
     private float scaleX = 1f;
@@ -134,9 +133,11 @@ public class AndroidTouchInput implements TouchInput {
             scaleX = settings.getWidth() / (float)androidInput.getView().getWidth();
             scaleY = settings.getHeight() / (float)androidInput.getView().getHeight();
         }
-        logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
-                new Object[]{scaleX, scaleY});
 
+        if (logger.isLoggable(Level.FINE)) {
+            logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
+                    new Object[]{scaleX, scaleY});
+        }
 
     }
 
@@ -162,7 +163,7 @@ public class AndroidTouchInput implements TouchInput {
         boolean bWasHandled = false;
         TouchEvent touch = null;
         //    System.out.println("native : " + event.getAction());
-        int action = getAction(event);
+        getAction(event);
         int pointerIndex = getPointerIndex(event);
         int pointerId = getPointerId(event);
         Vector2f lastPos = lastPositions.get(pointerId);
@@ -352,9 +353,9 @@ public class AndroidTouchInput implements TouchInput {
 //            logger.log(Level.FINE, "creating KeyInputEvent: {0}", kie);
         }
 
-        // consume all keys ourself except Volume Up/Down and Menu
+        // Consume all keys ourselves 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
+        //   by the user in MainActivity.
         if ((event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ||
                 (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN) ||
                 (event.getKeyCode() == KeyEvent.KEYCODE_MENU)) {
@@ -368,7 +369,7 @@ public class AndroidTouchInput implements TouchInput {
 
 
 
-        // -----------------------------------------
+    // -----------------------------------------
     // JME3 Input interface
     @Override
     public void initialize() {
@@ -469,7 +470,7 @@ public class AndroidTouchInput implements TouchInput {
 
     @Override
     public void setOmitHistoricEvents(boolean dontSendHistory) {
-        this.dontSendHistory = dontSendHistory;
+        // not implemented
     }
 
 }

+ 2 - 2
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -46,7 +46,7 @@ import java.util.logging.Logger;
  */
 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>();
+    final private HashMap<Integer, Vector2f> lastHoverPositions = new HashMap<>();
 
     public AndroidTouchInput14(AndroidInputHandler androidInput) {
         super(androidInput);

+ 35 - 0
jme3-android/src/main/java/com/jme3/input/android/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+/**
+ * user-input classes specific to Android devices
+ */
+package com.jme3.input.android;

+ 41 - 11
jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -45,6 +45,7 @@ import java.nio.ShortBuffer;
 public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
 
     IntBuffer tmpBuff = BufferUtils.createIntBuffer(1);
+    IntBuffer tmpBuff16 = BufferUtils.createIntBuffer(16);
 
     @Override
     public void resetStats() {
@@ -138,8 +139,8 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
     }
 
     @Override
-    public void glBufferData(int target, long data_size, int usage) {
-        GLES20.glBufferData(target, (int) data_size, null, usage);
+    public void glBufferData(int target, long dataSize, int usage) {
+        GLES20.glBufferData(target, (int) dataSize, null, usage);
     }
 
     @Override
@@ -317,6 +318,12 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
         return GLES20.glGetError();
     }
 
+    @Override
+    public void glGetFloat(int parameterId, FloatBuffer storeValues) {
+        checkLimit(storeValues);
+        GLES20.glGetFloatv(parameterId, storeValues);
+    }
+
     @Override
     public void glGetInteger(int pname, IntBuffer params) {
         checkLimit(params);
@@ -559,8 +566,8 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
     }
 
     @Override
-    public void glDrawElementsInstancedARB(int mode, int indices_count, int type, long indices_buffer_offset, int primcount) {
-        GLES30.glDrawElementsInstanced(mode, indices_count, type, (int)indices_buffer_offset, primcount);
+    public void glDrawElementsInstancedARB(int mode, int indicesCount, int type, long indicesBufferOffset, int primcount) {
+        GLES30.glDrawElementsInstanced(mode, indicesCount, type, (int)indicesBufferOffset, primcount);
     }
 
     @Override
@@ -574,8 +581,8 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
     }
 
     @Override
-    public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedsamplelocations) {
-        GLES31.glTexStorage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations);
+    public void glTexImage2DMultisample(int target, int samples, int internalformat, int width, int height, boolean fixedSampleLocations) {
+        GLES31.glTexStorage2DMultisample(target, samples, internalformat, width, height, fixedSampleLocations);
     }
 
     @Override
@@ -688,10 +695,17 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
     // Wrapper to DrawBuffers as there's no DrawBuffer method in GLES
     @Override
     public void glDrawBuffer(int mode) {
-        tmpBuff.clear();
-        tmpBuff.put(0, mode);
-        tmpBuff.rewind();
-        glDrawBuffers(tmpBuff);
+        int nBuffers = (mode - GLFbo.GL_COLOR_ATTACHMENT0_EXT) + 1;
+        if (nBuffers <= 0 || nBuffers > 16) {
+            throw new IllegalArgumentException("Draw buffer outside range: " + Integer.toHexString(mode));
+        }
+        tmpBuff16.clear();
+        for (int i = 0; i < nBuffers - 1; i++) {
+            tmpBuff16.put(GL.GL_NONE);
+        }
+        tmpBuff16.put(mode);
+        tmpBuff16.flip();
+        glDrawBuffers(tmpBuff16);
     }
 
     @Override
@@ -723,5 +737,21 @@ public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
         GLES30.glTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data);
     }
 
+    @Override
+    public void glBindVertexArray(int array) {
+        GLES30.glBindVertexArray(array);
+    }
+
+    @Override
+    public void glDeleteVertexArrays(IntBuffer arrays) {
+       GLES30.glDeleteVertexArrays(arrays.limit(),arrays);
+    }
+
+    @Override
+    public void glGenVertexArrays(IntBuffer arrays) {
+        GLES30.glGenVertexArrays(arrays.limit(),arrays);
+
+    }
+
 }
 

+ 9 - 1
jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2019 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -51,6 +51,12 @@ public class RendererUtil {
      */
     public static boolean ENABLE_ERROR_CHECKING = true;
 
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private RendererUtil() {
+    }
+
     /**
      * Checks for an OpenGL error and throws a {@link RendererException} if
      * there is one. Ignores the value of
@@ -71,6 +77,8 @@ public class RendererUtil {
     /**
      * Checks for an EGL error and throws a {@link RendererException} if there
      * is one. Ignores the value of {@link RendererUtil#ENABLE_ERROR_CHECKING}.
+     * 
+     * @param egl (not null)
      */
     public static void checkEGLError(EGL10 egl) {
         int error = egl.eglGetError();

+ 35 - 0
jme3-android/src/main/java/com/jme3/renderer/android/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 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.
+ */
+/**
+ * graphics-rendering code specific to Android devices
+ */
+package com.jme3.renderer.android;

+ 42 - 34
jme3-android/src/main/java/com/jme3/system/android/AndroidConfigChooser.java

@@ -35,70 +35,70 @@ public class AndroidConfigChooser implements EGLConfigChooser {
         EGLConfig[] configs = getConfigs(egl, display);
 
         // First try to find an exact match, but allowing a higher stencil
-        EGLConfig choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
-        if (choosenConfig == null && requestedConfig.d > 16) {
+        EGLConfig chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
+        if (chosenConfig == null && requestedConfig.d > 16) {
             logger.log(Level.INFO, "EGL configuration not found, reducing depth");
             requestedConfig.d = 16;
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, false, false, false, true);
         }
 
-        if (choosenConfig == null) {
+        if (chosenConfig == null) {
             logger.log(Level.INFO, "EGL configuration not found, allowing higher RGB");
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
         }
 
-        if (choosenConfig == null && requestedConfig.a > 0) {
+        if (chosenConfig == null && requestedConfig.a > 0) {
             logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
         }
 
-        if (choosenConfig == null && requestedConfig.s > 0) {
+        if (chosenConfig == null && requestedConfig.s > 0) {
             logger.log(Level.INFO, "EGL configuration not found, allowing higher samples");
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
         }
 
-        if (choosenConfig == null && requestedConfig.a > 0) {
+        if (chosenConfig == null && requestedConfig.a > 0) {
             logger.log(Level.INFO, "EGL configuration not found, reducing alpha");
             requestedConfig.a = 1;
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
         }
 
-        if (choosenConfig == null && requestedConfig.s > 0) {
+        if (chosenConfig == null && requestedConfig.s > 0) {
             logger.log(Level.INFO, "EGL configuration not found, reducing samples");
             requestedConfig.s = 1;
             if (requestedConfig.a > 0) {
-                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
+                chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, true, true);
             } else {
-                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true);
+                chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, true, true);
             }
         }
 
-        if (choosenConfig == null && requestedConfig.getBitsPerPixel() > 16) {
+        if (chosenConfig == null && requestedConfig.getBitsPerPixel() > 16) {
             logger.log(Level.INFO, "EGL configuration not found, setting to RGB565");
             requestedConfig.r = 5;
             requestedConfig.g = 6;
             requestedConfig.b = 5;
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
 
-            if (choosenConfig == null) {
+            if (chosenConfig == null) {
                 logger.log(Level.INFO, "EGL configuration not found, allowing higher alpha");
-                choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
+                chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, true, false, true);
             }
         }
 
-        if (choosenConfig == null) {
+        if (chosenConfig == null) {
             logger.log(Level.INFO, "EGL configuration not found, looking for best config with >= 16 bit Depth");
-            //failsafe, should pick best config with at least 16 depth
+            // failsafe: pick the best config with depth >= 16
             requestedConfig = new Config(0, 0, 0, 0, 16, 0, 0);
-            choosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
+            chosenConfig = chooseConfig(egl, display, configs, requestedConfig, true, false, false, true);
         }
 
-        if (choosenConfig != null) {
+        if (chosenConfig != null) {
             logger.fine("GLSurfaceView asks for egl config, returning: ");
-            logEGLConfig(choosenConfig, display, egl, Level.FINE);
+            logEGLConfig(chosenConfig, display, egl, Level.FINE);
 
-            storeSelectedConfig(egl, display, choosenConfig);
-            return choosenConfig;
+            storeSelectedConfig(egl, display, chosenConfig);
+            return chosenConfig;
         } else {
             logger.severe("No EGL Config found");
             return null;
@@ -118,11 +118,15 @@ public class AndroidConfigChooser implements EGLConfigChooser {
             g = 6;
             b = 5;
         }
-        logger.log(Level.FINE, "Requested Display Config:");
-        logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}",
-                new Object[]{settings.getBitsPerPixel(),
-                    settings.getAlphaBits(), settings.getDepthBits(),
-                    settings.getSamples(), settings.getStencilBits()});
+
+        if (logger.isLoggable(Level.FINE)) {
+            logger.log(Level.FINE, "Requested Display Config:");
+            logger.log(Level.FINE, "RGB: {0}, alpha: {1}, depth: {2}, samples: {3}, stencil: {4}",
+                    new Object[]{settings.getBitsPerPixel(),
+                            settings.getAlphaBits(), settings.getDepthBits(),
+                            settings.getSamples(), settings.getStencilBits()});
+        }
+
         return new Config(
                 r, g, b,
                 settings.getAlphaBits(),
@@ -214,8 +218,10 @@ public class AndroidConfigChooser implements EGLConfigChooser {
             int st = eglGetConfigAttribSafe(egl, display, config,
                     EGL10.EGL_STENCIL_SIZE);
 
-            logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
-                    new Object[]{r, g, b, a, d, s, st});
+            if (logger.isLoggable(Level.FINE)) {
+                logger.log(Level.FINE, "Checking Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
+                        new Object[]{r, g, b, a, d, s, st});
+            }
 
             if (higherRGB && r < requestedConfig.r) { continue; }
             if (!higherRGB && r != requestedConfig.r) { continue; }
@@ -243,8 +249,10 @@ public class AndroidConfigChooser implements EGLConfigChooser {
                 kr = r; kg = g; kb = b; ka = a;
                 kd = d; ks = s; kst = st;
                 keptConfig = config;
-                logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
-                        new Object[]{r, g, b, a, d, s, st});
+                if (logger.isLoggable(Level.FINE)) {
+                    logger.log(Level.FINE, "Keeping Config r: {0}, g: {1}, b: {2}, alpha: {3}, depth: {4}, samples: {5}, stencil: {6}",
+                            new Object[]{r, g, b, a, d, s, st});
+                }
             }
 
         }

+ 22 - 24
jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java

@@ -17,6 +17,8 @@ import com.jme3.audio.openal.EFX;
 import com.jme3.system.*;
 import com.jme3.system.JmeContext.Type;
 import com.jme3.util.AndroidScreenshots;
+import com.jme3.util.res.Resources;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -35,10 +37,22 @@ public class JmeAndroidSystem extends JmeSystemDelegate {
         } catch (UnsatisfiedLinkError e) {
         }
     }
+
+    public JmeAndroidSystem(){
+        setErrorMessageHandler((message) -> {
+            String finalMsg = message;
+            String finalTitle = "Error in application";
+            Context context = JmeAndroidSystem.getView().getContext();
+            view.getHandler().post(() -> {
+                AlertDialog dialog = new AlertDialog.Builder(context).setTitle(finalTitle).setMessage(finalMsg).create();
+                dialog.show();
+            });
+        });
+    }
     
     @Override
     public URL getPlatformAssetConfigURL() {
-        return Thread.currentThread().getContextClassLoader().getResource("com/jme3/asset/Android.cfg");
+        return Resources.getResource("com/jme3/asset/Android.cfg");
     }
 
     @Override
@@ -57,26 +71,8 @@ public class JmeAndroidSystem extends JmeSystemDelegate {
         bitmapImage.recycle();
     }
 
-    @Override
-    public void showErrorDialog(String message) {
-        final String finalMsg = message;
-        final String finalTitle = "Error in application";
-        final Context context = JmeAndroidSystem.getView().getContext();
 
-        view.getHandler().post(new Runnable() {
-            @Override
-            public void run() {
-                AlertDialog dialog = new AlertDialog.Builder(context)
-                        .setTitle(finalTitle).setMessage(finalMsg).create();
-                dialog.show();
-            }
-        });
-    }
 
-    @Override
-    public boolean showSettingsDialog(AppSettings sourceSettings, boolean loadFromRegistry) {
-        return true;
-    }
 
     @Override
     public JmeContext newContext(AppSettings settings, Type contextType) {
@@ -166,7 +162,7 @@ public class JmeAndroidSystem extends JmeSystemDelegate {
                 // When created this way, the directory is automatically removed by the Android
                 //   system when the app is uninstalled.
                 // The directory is also accessible by a PC connected to the device
-                //   so the files can be copied to the PC (ie. screenshots)
+                //   so the files can be copied to the PC (i.e. screenshots)
                 storageFolder = storageFolders.get(type);
                 if (storageFolder == null) {
                     String state = Environment.getExternalStorageState();
@@ -180,10 +176,12 @@ public class JmeAndroidSystem extends JmeSystemDelegate {
             default:
                 break;
         }
-        if (storageFolder != null) {
-            logger.log(Level.FINE, "Base Storage Folder Path: {0}", storageFolder.getAbsolutePath());
-        } else {
-            logger.log(Level.FINE, "Base Storage Folder not found!");
+        if (logger.isLoggable(Level.FINE)) {
+            if (storageFolder != null) {
+                logger.log(Level.FINE, "Base Storage Folder Path: {0}", storageFolder.getAbsolutePath());
+            } else {
+                logger.log(Level.FINE, "Base Storage Folder not found!");
+            }
         }
         return storageFolder;
     }

+ 84 - 8
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -37,10 +37,13 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.pm.ConfigurationInfo;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.opengl.GLSurfaceView;
 import android.os.Build;
 import android.text.InputType;
 import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.EditText;
@@ -54,8 +57,9 @@ import com.jme3.input.dummy.DummyMouseInput;
 import com.jme3.renderer.android.AndroidGL;
 import com.jme3.renderer.opengl.*;
 import com.jme3.system.*;
-import com.jme3.util.AndroidBufferAllocator;
 import com.jme3.util.BufferAllocatorFactory;
+import com.jme3.util.PrimitiveAllocator;
+
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -82,7 +86,7 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
         final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION;
 
         if (System.getProperty(implementation) == null) {
-            System.setProperty(implementation, AndroidBufferAllocator.class.getName());
+            System.setProperty(implementation, PrimitiveAllocator.class.getName());
         }
     }
 
@@ -101,6 +105,7 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
      * GLSurfaceView. Only one GLSurfaceView can be created at this time. The
      * given configType specifies how to determine the display configuration.
      *
+     * @param context (not null)
      * @return GLSurfaceView The newly created view
      */
     public GLSurfaceView createView(Context context) {
@@ -256,6 +261,16 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
         }
     }
 
+    /**
+     * Accesses the listener that receives events related to this context.
+     *
+     * @return the pre-existing instance
+     */
+    @Override
+    public SystemListener getSystemListener() {
+        return listener;
+    }
+
     @Override
     public void setSystemListener(SystemListener listener) {
         this.listener = listener;
@@ -313,11 +328,13 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
     // SystemListener:reshape
     @Override
     public void onSurfaceChanged(GL10 gl, int width, int height) {
-        logger.log(Level.FINE, "GL Surface changed, width: {0} height: {1}", new Object[]{width, height});
+        if (logger.isLoggable(Level.FINE)) {
+            logger.log(Level.FINE, "GL Surface changed, width: {0} height: {1}", new Object[]{width, height});
+        }
         // update the application settings with the new resolution
         settings.setResolution(width, height);
-        // reload settings in androidInput so the correct touch event scaling can be
-        // calculated in case the surface resolution is different than the view
+        // Reload settings in androidInput so the correct touch event scaling can be
+        // calculated in case the surface resolution is different than the view.
         androidInput.loadSettings(settings);
         // if the application has already been initialized (ie renderable is set)
         // then call reshape so the app can adjust to the new resolution.
@@ -411,8 +428,10 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
 
     @Override
     public void requestDialog(final int id, final String title, final String initialValue, final SoftTextDialogInputListener listener) {
-        logger.log(Level.FINE, "requestDialog: title: {0}, initialValue: {1}",
-                new Object[]{title, initialValue});
+        if (logger.isLoggable(Level.FINE)) {
+            logger.log(Level.FINE, "requestDialog: title: {0}, initialValue: {1}",
+                    new Object[]{title, initialValue});
+        }
 
         final View view = JmeAndroidSystem.getView();
         view.getHandler().post(new Runnable() {
@@ -479,4 +498,61 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
         logger.warning("OpenCL is not yet supported on android");
         return null;
     }
+
+    /**
+     * Returns the height of the input surface.
+     *
+     * @return the height (in pixels)
+     */
+    @Override
+    public int getFramebufferHeight() {
+        Rect rect = getSurfaceFrame();
+        int result = rect.height();
+        return result;
+    }
+
+    /**
+     * Returns the width of the input surface.
+     *
+     * @return the width (in pixels)
+     */
+    @Override
+    public int getFramebufferWidth() {
+        Rect rect = getSurfaceFrame();
+        int result = rect.width();
+        return result;
+    }
+
+    /**
+     * Returns the screen X coordinate of the left edge of the content area.
+     *
+     * @throws UnsupportedOperationException
+     */
+    @Override
+    public int getWindowXPosition() {
+        throw new UnsupportedOperationException("not implemented yet");
+    }
+
+    /**
+     * Returns the screen Y coordinate of the top edge of the content area.
+     *
+     * @throws UnsupportedOperationException
+     */
+    @Override
+    public int getWindowYPosition() {
+        throw new UnsupportedOperationException("not implemented yet");
+    }
+    
+    /**
+     * Retrieves the dimensions of the input surface. Note: do not modify the
+     * returned object.
+     * 
+     * @return the dimensions (in pixels, left and top are 0)
+     */
+    private Rect getSurfaceFrame() {
+        SurfaceView view = (SurfaceView) androidInput.getView();
+        SurfaceHolder holder = view.getHolder();
+        Rect result = holder.getSurfaceFrame();
+        return result;
+    }
 }

+ 18 - 13
jme3-android/src/main/java/com/jme3/texture/plugins/AndroidBufferImageLoader.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -41,16 +41,18 @@ import com.jme3.texture.Image;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.BufferUtils;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.BufferedInputStream;
 import java.nio.ByteBuffer;
 
 /**
  * Loads textures using Android's Bitmap class, but does not have the 
  * RGBA8 alpha bug.
+ *
+ * See below link for supported image formats:
+ * https://developer.android.com/guide/topics/media/media-formats#image-formats
  * 
  * @author Kirill Vainer
  */
-@Deprecated
 public class AndroidBufferImageLoader implements AssetLoader {
     
     private final byte[] tempData = new byte[16 * 1024];
@@ -69,9 +71,8 @@ public class AndroidBufferImageLoader implements AssetLoader {
     
     @Override
     public Object load(AssetInfo assetInfo) throws IOException {
-        Bitmap bitmap = null;
+        Bitmap bitmap;
         Image.Format format;
-        InputStream in = null;
         int bpp;
         
         BitmapFactory.Options options = new BitmapFactory.Options();
@@ -83,17 +84,20 @@ public class AndroidBufferImageLoader implements AssetLoader {
         options.inInputShareable = true;
         options.inPurgeable = true;
         options.inSampleSize = 1;
-        
-        try {
-            in = assetInfo.openStream();
-            bitmap = BitmapFactory.decodeStream(in, null, options);
+        // Do not premultiply alpha channel as it is not intended
+        // to be directly drawn by the android view system.
+        options.inPremultiplied = false;
+
+        // TODO: It is more GC friendly to reuse the Bitmap class instead of recycling
+        //  it on every image load. Android has introduced inBitmap option For this purpose.
+        //  However, there are certain restrictions with how inBitmap can be used.
+        //  See https://developer.android.com/topic/performance/graphics/manage-memory#inBitmap.
+
+        try (final BufferedInputStream bin = new BufferedInputStream(assetInfo.openStream())) {
+            bitmap = BitmapFactory.decodeStream(bin, null, options);
             if (bitmap == null) {
                 throw new IOException("Failed to load image: " + assetInfo.getKey().getName());
             }
-        } finally {
-            if (in != null) {
-                in.close();
-            }
         }
 
         switch (bitmap.getConfig()) {
@@ -160,6 +164,7 @@ public class AndroidBufferImageLoader implements AssetLoader {
         bitmap.recycle();
         
         Image image = new Image(format, width, height, data, ColorSpace.sRGB);
+        
         return image;
     }
 }

+ 32 - 9
jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java

@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2009-2021 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.texture.plugins;
 
 import com.jme3.asset.AssetInfo;
@@ -9,8 +40,6 @@ import java.io.InputStream;
 
 /**
  * Native image loader to deal with filetypes that support alpha channels.
- * The Android Bitmap class premultiplies the channels by the alpha when
- * loading.  This loader does not.
  *
  * @author iwgeric
  * @author Kirill Vainer
@@ -28,14 +57,8 @@ public class AndroidNativeImageLoader  implements AssetLoader {
     @Override
     public Image load(AssetInfo info) throws IOException {
         boolean flip = ((TextureKey) info.getKey()).isFlipY();
-        InputStream in = null;
-        try {
-            in = info.openStream();
+        try (final InputStream in = info.openStream()) {
             return load(in, flip, tmpArray);
-        } finally {
-            if (in != null){
-                in.close();
-            }
         }
     }
 }

+ 5 - 3
jme3-android/src/main/java/com/jme3/util/AndroidBufferAllocator.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2019 jMonkeyEngine
+ * Copyright (c) 2009-2022 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,7 +40,9 @@ import java.util.Map;
 
 /**
  * @author Jesus Oliver
+ * @deprecated implemented {@link AndroidNativeBufferAllocator} instead.
  */
+@Deprecated
 public class AndroidBufferAllocator implements BufferAllocator {
 
     // We make use of the ReflectionAllocator to remove the inner buffer
@@ -79,14 +81,14 @@ public class AndroidBufferAllocator implements BufferAllocator {
         }
     }
 
-    @Override
     /**
-     * This function search the inner direct buffer of the android specific wrapped buffer classes
+     * Searches the inner direct buffer of the Android-specific wrapped buffer classes
      * and destroys it using the reflection allocator method.
      *
      * @param toBeDestroyed The direct buffer that will be "cleaned".
      *
      */
+    @Override
     public void destroyDirectBuffer(Buffer toBeDestroyed) {
         // If it is a wrapped buffer, get it's inner direct buffer field and destroy it
         Field field = fieldIndex.get(toBeDestroyed.getClass());

+ 3 - 0
jme3-android/src/main/java/com/jme3/util/AndroidLogHandler.java

@@ -90,6 +90,9 @@ public class AndroidLogHandler extends Handler {
      * Returns the short logger tag for the given logger name.
      * Traditionally loggers are named by fully-qualified Java classes; this
      * method attempts to return a concise identifying part of such names.
+     * 
+     * @param loggerName the logger name, or null for anonymous
+     * @return the short logger tag
      */
     public static String loggerNameToTag(String loggerName) {
         // Anonymous logger.

+ 74 - 0
jme3-android/src/main/java/com/jme3/util/AndroidNativeBufferAllocator.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2009-2022 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.util;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+
+/**
+ * Allocates and destroys direct byte buffers using native code.
+ *
+ * @author pavl_g.
+ */
+public final class AndroidNativeBufferAllocator implements BufferAllocator {
+
+    static {
+        System.loadLibrary("bufferallocatorjme");
+    }
+
+    @Override
+    public void destroyDirectBuffer(Buffer toBeDestroyed) {
+        releaseDirectByteBuffer(toBeDestroyed);
+    }
+
+    @Override
+    public ByteBuffer allocate(int size) {
+        return createDirectByteBuffer(size);
+    }
+
+    /**
+     * Releases the memory of a direct buffer using a buffer object reference.
+     *
+     * @param buffer the buffer reference to release its memory.
+     * @see AndroidNativeBufferAllocator#destroyDirectBuffer(Buffer)
+     */
+    private native void releaseDirectByteBuffer(Buffer buffer);
+
+    /**
+     * Creates a new direct byte buffer explicitly with a specific size.
+     *
+     * @param size the byte buffer size used for allocating the buffer.
+     * @return a new direct byte buffer object.
+     * @see AndroidNativeBufferAllocator#allocate(int)
+     */
+    private native ByteBuffer createDirectByteBuffer(long size);
+}

+ 6 - 0
jme3-android/src/main/java/com/jme3/util/AndroidScreenshots.java

@@ -8,6 +8,12 @@ public final class AndroidScreenshots {
 
     private static final Logger logger = Logger.getLogger(AndroidScreenshots.class.getName());
 
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private AndroidScreenshots() {
+    }
+
     /**
      * Convert OpenGL GLES20.GL_RGBA to Bitmap.Config.ARGB_8888 and store result
      * in a Bitmap

+ 36 - 0
jme3-android/src/main/java/com/jme3/view/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2009-2022 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.
+ */
+/**
+ * Provides classes that expose custom android-native ui that can handle screen layout and interactions with the user
+ * for a jMonkeyEngine game.
+ */
+package com.jme3.view;

+ 1014 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java

@@ -0,0 +1,1014 @@
+/*
+ * Copyright (c) 2009-2022 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.view.surfaceview;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLSurfaceView;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
+import com.jme3.app.LegacyApplication;
+import com.jme3.asset.AssetLoader;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.input.JoyInput;
+import com.jme3.input.android.AndroidSensorJoyInput;
+import com.jme3.scene.Spatial;
+import com.jme3.system.AppSettings;
+import com.jme3.system.SystemListener;
+import com.jme3.system.android.JmeAndroidSystem;
+import com.jme3.system.android.OGLESContext;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <b>A RelativeLayout class holder that wraps a {@link GLSurfaceView} as a renderer UI component and uses {@link OGLESContext} as a renderer context to render
+ * a jme game on an android view for custom xml designs.</b>
+ * The main idea of {@link JmeSurfaceView} class is to start a jMonkeyEngine application in a {@link SystemListener} context on a GL_ES thread,
+ * then the game is rendered and updated through a {@link GLSurfaceView} component with a delay of user's choice using a {@link Handler}, during the delay,
+ * the user has the ability to handle a couple of actions asynchronously as displaying a progress bar on a SplashScreen or an image or even play a preface game music of choice.
+ *
+ * @author pavl_g.
+ */
+public class JmeSurfaceView extends RelativeLayout implements SystemListener, DialogInterface.OnClickListener, LifecycleEventObserver {
+
+    private static final Logger jmeSurfaceViewLogger = Logger.getLogger(JmeSurfaceView.class.getName());
+    /*AppSettings attributes*/
+    protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT;
+    /*using {@link LegacyApplication} instead of {@link SimpleApplication} to include all classes extends LegacyApplication*/
+    private LegacyApplication legacyApplication;
+    private AppSettings appSettings;
+    private int eglBitsPerPixel = 24;
+    private int eglAlphaBits = 0;
+    private int eglDepthBits = 16;
+    private int eglSamples = 0;
+    private int eglStencilBits = 0;
+    private int frameRate = -1;
+    private boolean emulateKeyBoard = true;
+    private boolean emulateMouse = true;
+    private boolean useJoyStickEvents = true;
+    private boolean isGLThreadPaused;
+    /*Late-init instances -- nullable objects*/
+    private GLSurfaceView glSurfaceView;
+    private OGLESContext oglesContext;
+    private ConfigurationInfo configurationInfo;
+    private OnRendererCompleted onRendererCompleted;
+    private OnRendererStarted onRendererStarted;
+    private OnExceptionThrown onExceptionThrown;
+    private OnLayoutDrawn onLayoutDrawn;
+    /*Global Objects*/
+    private Handler handler = new Handler();
+    private RendererThread rendererThread = new RendererThread();
+    private StringWriter crashLogWriter = new StringWriter(150);
+    /*Global flags*/
+    private boolean showErrorDialog = true;
+    private boolean bindAppState = true;
+    private boolean showEscExitPrompt = true;
+    private boolean exitOnEscPressed = true;
+    /*Destruction policy flag*/
+    private DestructionPolicy destructionPolicy = DestructionPolicy.DESTROY_WHEN_FINISH;
+    /*extra messages/data*/
+    private String crashLog = "";
+    private String glEsVersion = "";
+
+    /**
+     * Instantiates a default surface view holder without XML attributes.
+     * On instantiating this surface view, the holder is bound directly to the
+     * parent context life cycle.
+     *
+     * @param context the parent context.
+     */
+    public JmeSurfaceView(@NonNull Context context) {
+        super(context);
+        //binds the view component to the holder activity life cycle
+        bindAppStateToActivityLifeCycle(bindAppState);
+    }
+
+    /**
+     * Instantiates a surface view holder with XML attributes from an XML document.
+     * On instantiating this surface view, the holder is bound directly to the
+     * parent context life cycle.
+     *
+     * @param context the parent context.
+     * @param attrs   a collection of attributes describes the tags in an XML document.
+     * @see android.content.res.Resources.Theme#obtainAttributes(AttributeSet, int[])
+     */
+    public JmeSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        //binds the view component to the holder activity life cycle
+        bindAppStateToActivityLifeCycle(bindAppState);
+    }
+
+    /**
+     * Instantiates a surface view holder with XML attributes and a default style attribute.
+     * On instantiating this surface view, the holder is bound directly to the
+     * parent context life cycle.
+     *
+     * @param context      the parent context.
+     * @param attrs        a collection of attributes describes the tags in an XML document.
+     * @param defStyleAttr an attribute in the current theme that contains a
+     *                     reference to a style resource that supplies
+     *                     defaults values. Can be 0 to not look for defaults.
+     * @see android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+     */
+    public JmeSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        //binds the view component to the holder activity life cycle
+        bindAppStateToActivityLifeCycle(bindAppState);
+    }
+
+    /**
+     * Instantiates a surface view holder with XML attributes, default style attribute and a default style resource.
+     * On instantiating this surface view, the holder is bound directly to the
+     * parent context life cycle.
+     *
+     * @param context      the parent context.
+     * @param attrs        a collection of attributes describes the tags in an XML document.
+     * @param defStyleAttr an attribute in the current theme that contains defaults. Can be 0 to not look for defaults.
+     * @param defStyleRes  a resource identifier of a style resource that
+     *                     supplies default values, used only if defStyleAttr is 0 or can not be found in the theme.
+     *                     Can be 0 to not look for defaults.
+     * @see android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+     */
+    public JmeSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        //binds the view component to the holder activity life cycle
+        bindAppStateToActivityLifeCycle(bindAppState);
+    }
+
+    /**
+     * Starts the jmeRenderer on a GlSurfaceView attached to a RelativeLayout.
+     *
+     * @param delayMillis delays the attachment of the surface view to the UI (RelativeLayout).
+     */
+    public void startRenderer(int delayMillis) {
+        delayMillis = Math.max(0, delayMillis);
+        /*gets the device configuration attributes from the activity manager*/
+        configurationInfo = ((ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE)).getDeviceConfigurationInfo();
+        glEsVersion = "GL_ES Version : " + configurationInfo.getGlEsVersion();
+        /*sanity check the app instance*/
+        if (legacyApplication == null) {
+            throw new IllegalStateException("Cannot build a SurfaceView for a null app, make sure to use setLegacyApplication() to pass in your app !");
+        }
+        /*initialize App Settings and start the Game*/
+        appSettings = new AppSettings(true);
+        appSettings.setAudioRenderer(audioRendererType);
+        appSettings.setResolution(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height);
+        appSettings.setAlphaBits(eglAlphaBits);
+        appSettings.setDepthBits(eglDepthBits);
+        appSettings.setSamples(eglSamples);
+        appSettings.setStencilBits(eglStencilBits);
+        appSettings.setBitsPerPixel(eglBitsPerPixel);
+        appSettings.setEmulateKeyboard(emulateKeyBoard);
+        appSettings.setEmulateMouse(emulateMouse);
+        appSettings.setUseJoysticks(useJoyStickEvents);
+        /*fetch and sanity check the static memory*/
+        if (GameState.getLegacyApplication() != null) {
+            this.legacyApplication = GameState.getLegacyApplication();
+            jmeSurfaceViewLogger.log(Level.INFO, "Old game state has been assigned as the current game state, skipping the first update");
+        } else {
+            legacyApplication.setSettings(appSettings);
+            jmeSurfaceViewLogger.log(Level.INFO, "Starting a new Game State");
+            /*start jme game context*/
+            legacyApplication.start();
+            /*fire the onStart() listener*/
+            if (onRendererStarted != null) {
+                onRendererStarted.onRenderStart(legacyApplication, this);
+            }
+        }
+        /*attach the game to JmE OpenGL.Renderer context*/
+        oglesContext = (OGLESContext) legacyApplication.getContext();
+        /*create a glSurfaceView that will hold the renderer thread*/
+        glSurfaceView = oglesContext.createView(JmeSurfaceView.this.getContext());
+        /*set the current view as the system engine thread view for future uses*/
+        JmeAndroidSystem.setView(JmeSurfaceView.this);
+        /*set JME system Listener to initialize game, update, requestClose and destroy on closure*/
+        oglesContext.setSystemListener(JmeSurfaceView.this);
+        /*set the glSurfaceView to fit the widget*/
+        glSurfaceView.setLayoutParams(new LayoutParams(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height));
+        if (GameState.getLegacyApplication() != null) {
+            addGlSurfaceView();
+        } else {
+            /*post delay the attachment of the surface view on the UI*/
+            handler.postDelayed(rendererThread, delayMillis);
+        }
+    }
+
+    private void removeGLSurfaceView() {
+        ((Activity) getContext()).runOnUiThread(() -> {
+            if (glSurfaceView != null) {
+                JmeSurfaceView.this.removeView(glSurfaceView);
+            }
+        });
+    }
+
+    @Override
+    public void handleError(String errorMsg, Throwable throwable) {
+        throwable.printStackTrace();
+        showErrorDialog(throwable, throwable.getClass().getName());
+        if (onExceptionThrown != null) {
+            onExceptionThrown.onExceptionThrown(throwable);
+        }
+    }
+
+    /**
+     * A state change observer to the holder Activity life cycle, used to keep this android view up-to-date with the holder activity life cycle.
+     *
+     * @param source the life cycle source, aka the observable object.
+     * @param event  the fired event by the observable object, which is dispatched and sent to the observers.
+     */
+    @Override
+    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
+        switch (event) {
+            case ON_DESTROY:
+                /*destroy only if the policy flag is enabled*/
+                if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
+                    legacyApplication.stop(!isGLThreadPaused());
+                }
+                break;
+            case ON_PAUSE:
+                loseFocus();
+                break;
+            case ON_RESUME:
+                gainFocus();
+                break;
+            case ON_STOP:
+                jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running");
+                break;
+        }
+    }
+
+    @Override
+    public void initialize() {
+        /*Invoking can be delayed by delaying the draw of GlSurfaceView component on the screen*/
+        if (legacyApplication == null) {
+            return;
+        }
+        legacyApplication.initialize();
+        /*log for display*/
+        jmeSurfaceViewLogger.log(Level.INFO, "JmeGame started in GLThread Asynchronously.......");
+    }
+
+    @Override
+    public void reshape(int width, int height) {
+        if (legacyApplication == null) {
+            return;
+        }
+        legacyApplication.reshape(width, height);
+        jmeSurfaceViewLogger.log(Level.INFO, "Requested reshaping from the system listener");
+    }
+
+    @Override
+    public void rescale(float x, float y) {
+        if (legacyApplication == null) {
+            return;
+        }
+        legacyApplication.rescale(x, y);
+        jmeSurfaceViewLogger.log(Level.INFO, "Requested rescaling from the system listener");
+    }
+
+    @Override
+    public void update() {
+        /*Invoking can be delayed by delaying the draw of GlSurfaceView component on the screen*/
+        if (legacyApplication == null || glSurfaceView == null) {
+            return;
+        }
+        legacyApplication.update();
+        if (!GameState.isFirstUpdatePassed()) {
+            ((Activity) getContext()).runOnUiThread(() -> {
+                jmeSurfaceViewLogger.log(Level.INFO, "User delay finishes with 0 errors");
+                if (onRendererCompleted != null) {
+                    onRendererCompleted.onRenderCompletion(legacyApplication, legacyApplication.getContext().getSettings());
+                }
+            });
+            GameState.setFirstUpdatePassed(true);
+        }
+    }
+
+    @Override
+    public void requestClose(boolean esc) {
+        /*skip if it's not enabled or the input is null*/
+        if (legacyApplication == null || (!isExitOnEscPressed())) {
+            return;
+        }
+        if (isShowEscExitPrompt()) {
+            final AlertDialog alertDialog = new AlertDialog.Builder(getContext()).create();
+            alertDialog.setTitle("Exit Prompt");
+            alertDialog.setMessage("Are you sure you want to quit ?");
+            alertDialog.setCancelable(false);
+            alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "No", (dialogInterface, i) -> alertDialog.dismiss());
+            alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Yes", (dialogInterface, i) -> legacyApplication.requestClose(esc));
+            alertDialog.show();
+        } else {
+            legacyApplication.requestClose(esc);
+        }
+    }
+
+    @Override
+    public void gainFocus() {
+        /*skip the block if the instances are nullptr*/
+        if (legacyApplication == null || glSurfaceView == null) {
+            return;
+        }
+        glSurfaceView.onResume();
+        /*resume the audio*/
+        final AudioRenderer audioRenderer = legacyApplication.getAudioRenderer();
+        if (audioRenderer != null) {
+            audioRenderer.resumeAll();
+        }
+        /*resume the sensors (aka joysticks)*/
+        if (legacyApplication.getContext() != null) {
+            final JoyInput joyInput = legacyApplication.getContext().getJoyInput();
+            if (joyInput != null) {
+                if (joyInput instanceof AndroidSensorJoyInput) {
+                    final AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
+                    androidJoyInput.resumeSensors();
+                }
+            }
+            legacyApplication.gainFocus();
+        }
+        setGLThreadPaused(false);
+        jmeSurfaceViewLogger.log(Level.INFO, "Game returns from the idle mode");
+    }
+
+    @Override
+    public void loseFocus() {
+        /*skip the block if the invoking instances are nullptr*/
+        if (legacyApplication == null || glSurfaceView == null) {
+            return;
+        }
+        glSurfaceView.onPause();
+        /*pause the audio*/
+        legacyApplication.loseFocus();
+        final AudioRenderer audioRenderer = legacyApplication.getAudioRenderer();
+        if (audioRenderer != null) {
+            audioRenderer.pauseAll();
+        }
+        /*pause the sensors (aka joysticks)*/
+        if (legacyApplication.getContext() != null) {
+            final JoyInput joyInput = legacyApplication.getContext().getJoyInput();
+            if (joyInput != null) {
+                if (joyInput instanceof AndroidSensorJoyInput) {
+                    final AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput;
+                    androidJoyInput.pauseSensors();
+                }
+            }
+        }
+        setGLThreadPaused(true);
+        jmeSurfaceViewLogger.log(Level.INFO, "Game goes idle");
+    }
+
+    @Override
+    public void destroy() {
+        /*skip the destroy block if the invoking instance is null*/
+        if (legacyApplication == null) {
+            return;
+        }
+        removeGLSurfaceView();
+        legacyApplication.destroy();
+        /*help the Dalvik Garbage collector to destruct the pointers, by making them nullptr*/
+        /*context instances*/
+        legacyApplication = null;
+        appSettings = null;
+        oglesContext = null;
+        configurationInfo = null;
+        /*extra data instances*/
+        crashLogWriter = null;
+        crashLog = null;
+        /*nullifying helper instances and flags*/
+        rendererThread = null;
+        destructionPolicy = null;
+        audioRendererType = null;
+        handler = null;
+        glEsVersion = null;
+        /*nullifying the event handlers*/
+        onRendererStarted = null;
+        onRendererCompleted = null;
+        onExceptionThrown = null;
+        onLayoutDrawn = null;
+        /*nullifying the static memory (pushing zero to registers to prepare for a clean use)*/
+        GameState.setLegacyApplication(null);
+        GameState.setFirstUpdatePassed(false);
+        jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed");
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        switch (which) {
+            case DialogInterface.BUTTON_NEGATIVE:
+                dialog.dismiss();
+                ((Activity) getContext()).finish();
+                break;
+            case DialogInterface.BUTTON_POSITIVE:
+                dialog.dismiss();
+                break;
+            case DialogInterface.BUTTON_NEUTRAL:
+                /*copy crash log button*/
+                final ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+                final ClipData clipData = ClipData.newPlainText("Crash Log", crashLog);
+                clipboardManager.setPrimaryClip(clipData);
+                Toast.makeText(getContext(), "Crash Log copied to clipboard", Toast.LENGTH_SHORT).show();
+                break;
+        }
+    }
+
+    /**
+     * Adds the glSurfaceView to the screen immediately, saving the current app instance.
+     */
+    protected void addGlSurfaceView() {
+        /*jme Renderer joins the UIThread at that point*/
+        JmeSurfaceView.this.addView(glSurfaceView);
+        /*dispatch the layout drawn event*/
+        if (onLayoutDrawn != null) {
+            onLayoutDrawn.onLayoutDrawn(legacyApplication, this);
+        }
+        /*set the static memory to hold the game state, only if the destruction policy uses KEEP_WHEN_FINISHED policy*/
+        if (destructionPolicy == DestructionPolicy.KEEP_WHEN_FINISH) {
+            GameState.setLegacyApplication(legacyApplication);
+        } else {
+            GameState.setLegacyApplication(null);
+        }
+    }
+
+    /**
+     * Displays an error dialog with a throwable title(error/exception), message and 3 buttons.
+     * 1st button is : EXIT to exit the activity and terminates the app.
+     * 2nd button is : DISMISS to dismiss the dialog and ignore the exception.
+     * 3rd button is : CopyCrashLog to copy the crash log to the clipboard.
+     *
+     * @param throwable the throwable stack.
+     * @param title     the message title.
+     */
+    protected void showErrorDialog(Throwable throwable, String title) {
+        if (!isShowErrorDialog()) {
+            return;
+        }
+        ((Activity) getContext()).runOnUiThread(() -> {
+            throwable.printStackTrace(new PrintWriter(crashLogWriter));
+            crashLog = glEsVersion + "\n" + crashLogWriter.toString();
+
+            final AlertDialog alertDialog = new AlertDialog.Builder(getContext()).create();
+            alertDialog.setTitle(glEsVersion + ", " + title);
+            alertDialog.setMessage(crashLog);
+            alertDialog.setCancelable(false);
+            alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Exit", this);
+            alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Dismiss", this);
+            alertDialog.setButton(DialogInterface.BUTTON_NEUTRAL, "Copy crash log", this);
+            alertDialog.show();
+        });
+    }
+
+    /**
+     * Binds/Unbinds the game life cycle to the holder activity life cycle.
+     * Unbinding the game life cycle, would disable {@link JmeSurfaceView#gainFocus()}, {@link JmeSurfaceView#loseFocus()}
+     * and {@link JmeSurfaceView#destroy()} from being invoked by the System Listener.
+     * The Default value is : true, and the view component is pre-bounded to its activity lifeCycle when initialized.
+     *
+     * @param condition true if you want to bind them, false otherwise.
+     */
+    public void bindAppStateToActivityLifeCycle(final boolean condition) {
+        this.bindAppState = condition;
+        if (condition) {
+            /*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
+            if (getContext() instanceof LifecycleOwner) {
+                ((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
+            }
+        } else {
+            /*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
+            if (getContext() instanceof LifecycleOwner) {
+                ((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
+            }
+        }
+    }
+
+    /**
+     * Gets the current destruction policy.
+     * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
+     *
+     * @return the destruction policy, either {@link DestructionPolicy#DESTROY_WHEN_FINISH} or {@link DestructionPolicy#KEEP_WHEN_FINISH}.
+     * @see DestructionPolicy
+     * @see GameState
+     */
+    public DestructionPolicy getDestructionPolicy() {
+        return destructionPolicy;
+    }
+
+    /**
+     * Sets the current destruction policy, destruction policy {@link DestructionPolicy#KEEP_WHEN_FINISH} ensures that we protect the app state
+     * using {@link GameState#legacyApplication} static memory when the activity finishes, while
+     * {@link DestructionPolicy#DESTROY_WHEN_FINISH} destroys the game context with the activity onDestroy().
+     * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
+     *
+     * @param destructionPolicy a destruction policy to set.
+     * @see DestructionPolicy
+     * @see GameState
+     */
+    public void setDestructionPolicy(DestructionPolicy destructionPolicy) {
+        this.destructionPolicy = destructionPolicy;
+    }
+
+    /**
+     * Checks whether the current game application life cycle is bound to the activity life cycle.
+     *
+     * @return true it matches the condition, false otherwise.
+     */
+    public boolean isAppStateBoundToActivityLifeCycle() {
+        return bindAppState;
+    }
+
+    /**
+     * Checks whether the system would show an exit prompt dialog when the esc keyboard input is invoked.
+     *
+     * @return ture if the exit prompt dialog is activated on exit, false otherwise.
+     */
+    public boolean isShowEscExitPrompt() {
+        return showEscExitPrompt;
+    }
+
+    /**
+     * Determines whether to show an exit prompt dialog when the esc keyboard button is invoked.
+     *
+     * @param showEscExitPrompt true to show the exit prompt dialog before exiting, false otherwise.
+     */
+    public void setShowEscExitPrompt(boolean showEscExitPrompt) {
+        this.showEscExitPrompt = showEscExitPrompt;
+    }
+
+    /**
+     * Checks whether the exit on esc press is activated.
+     *
+     * @return true if the exit on escape is activated, false otherwise.
+     */
+    public boolean isExitOnEscPressed() {
+        return exitOnEscPressed;
+    }
+
+    /**
+     * Determines whether the system would exit on pressing the keyboard esc button.
+     *
+     * @param exitOnEscPressed true to activate exiting on Esc button press, false otherwise.
+     */
+    public void setExitOnEscPressed(boolean exitOnEscPressed) {
+        this.exitOnEscPressed = exitOnEscPressed;
+    }
+
+    /**
+     * Gets the jme app instance.
+     *
+     * @return legacyApplication instance representing your game enclosure.
+     */
+    public LegacyApplication getLegacyApplication() {
+        return legacyApplication;
+    }
+
+    /**
+     * Sets the jme game instance that will be engaged into the {@link SystemListener}.
+     *
+     * @param legacyApplication your jme game instance.
+     */
+    public void setLegacyApplication(@NonNull LegacyApplication legacyApplication) {
+        this.legacyApplication = legacyApplication;
+    }
+
+    /**
+     * Gets the game window settings.
+     *
+     * @return app settings instance.
+     */
+    public AppSettings getAppSettings() {
+        return appSettings;
+    }
+
+    /**
+     * Sets the appSettings instance.
+     *
+     * @param appSettings the custom appSettings instance
+     */
+    public void setAppSettings(@NonNull AppSettings appSettings) {
+        this.appSettings = appSettings;
+    }
+
+    /**
+     * Gets the bits/pixel for Embedded gL
+     *
+     * @return integer representing it.
+     */
+    public int getEglBitsPerPixel() {
+        return eglBitsPerPixel;
+    }
+
+    /**
+     * Sets the memory representing each pixel in bits.
+     *
+     * @param eglBitsPerPixel the bits for each pixel.
+     */
+    public void setEglBitsPerPixel(int eglBitsPerPixel) {
+        this.eglBitsPerPixel = eglBitsPerPixel;
+    }
+
+    /**
+     * Gets the Embedded gL alpha(opacity) bits.
+     *
+     * @return integer representing it.
+     */
+    public int getEglAlphaBits() {
+        return eglAlphaBits;
+    }
+
+    /**
+     * Sets the memory representing the alpha of embedded gl in bits.
+     *
+     * @param eglAlphaBits the alpha bits.
+     */
+    public void setEglAlphaBits(int eglAlphaBits) {
+        this.eglAlphaBits = eglAlphaBits;
+    }
+
+    /**
+     * Gets the memory representing the EGL depth in bits.
+     *
+     * @return the depth bits.
+     */
+    public int getEglDepthBits() {
+        return eglDepthBits;
+    }
+
+    /**
+     * Sets the EGL depth in bits.
+     * The depth buffer or Z-buffer is basically coupled with stencil buffer,
+     * usually 8bits stencilBuffer + 24bits depthBuffer = 32bits shared memory.
+     *
+     * @param eglDepthBits the depth bits.
+     * @see JmeSurfaceView#setEglStencilBits(int)
+     */
+    public void setEglDepthBits(int eglDepthBits) {
+        this.eglDepthBits = eglDepthBits;
+    }
+
+    /**
+     * Gets the number of samples to use for multi-sampling.
+     *
+     * @return number of samples to use for multi-sampling.
+     */
+    public int getEglSamples() {
+        return eglSamples;
+    }
+
+    /**
+     * Sets the number of samples to use for multi-sampling.
+     * Leave 0 (default) to disable multi-sampling.
+     * Set to 2 or 4 to enable multi-sampling.
+     *
+     * @param eglSamples embedded gl samples bits to set.
+     */
+    public void setEglSamples(int eglSamples) {
+        this.eglSamples = eglSamples;
+    }
+
+    /**
+     * Gets the number of stencil buffer bits.
+     * Default is : 0.
+     *
+     * @return the stencil buffer bits.
+     */
+    public int getEglStencilBits() {
+        return eglStencilBits;
+    }
+
+    /**
+     * Sets the number of stencil buffer bits.
+     * Stencil buffer is used in depth-based shadow maps and shadow rendering as it limits rendering,
+     * it's coupled with Z-buffer or depth buffer, usually 8bits stencilBuffer + 24bits depthBuffer = 32bits shared memory.
+     * (default = 0)
+     *
+     * @param eglStencilBits the desired number of stencil bits.
+     * @see JmeSurfaceView#setEglDepthBits(int)
+     */
+    public void setEglStencilBits(int eglStencilBits) {
+        this.eglStencilBits = eglStencilBits;
+    }
+
+    /**
+     * Gets the limited FrameRate level for egl INFO.
+     * Default is : -1, for a device based limited value (determined by hardware).
+     *
+     * @return the limit frameRate in integers.
+     */
+    public int getFrameRate() {
+        return frameRate;
+    }
+
+    /**
+     * Limits the frame rate (fps) in the second.
+     * Default is : -1, for a device based limited value (determined by hardware).
+     *
+     * @param frameRate the limitation in integers.
+     */
+    public void setFrameRate(int frameRate) {
+        this.frameRate = frameRate;
+    }
+
+    /**
+     * Gets the audio renderer in String.
+     * Default is : {@link AppSettings#ANDROID_OPENAL_SOFT}.
+     *
+     * @return string representing audio renderer framework.
+     */
+    public String getAudioRendererType() {
+        return audioRendererType;
+    }
+
+    /**
+     * Sets the audioRenderer type.
+     * Default is : {@link AppSettings#ANDROID_OPENAL_SOFT}.
+     *
+     * @param audioRendererType string representing audioRenderer type.
+     */
+    public void setAudioRendererType(String audioRendererType) {
+        this.audioRendererType = audioRendererType;
+    }
+
+    /**
+     * Checks if the keyboard interfacing is enabled.
+     * Default is : true.
+     *
+     * @return true if the keyboard interfacing is enabled.
+     */
+    public boolean isEmulateKeyBoard() {
+        return emulateKeyBoard;
+    }
+
+    /**
+     * Enables keyboard interfacing.
+     * Default is : true.
+     *
+     * @param emulateKeyBoard true to enable keyboard interfacing.
+     */
+    public void setEmulateKeyBoard(boolean emulateKeyBoard) {
+        this.emulateKeyBoard = emulateKeyBoard;
+    }
+
+    /**
+     * Checks whether the mouse interfacing is enabled or not.
+     * Default is : true.
+     *
+     * @return true if the mouse interfacing is enabled.
+     */
+    public boolean isEmulateMouse() {
+        return emulateMouse;
+    }
+
+    /**
+     * Enables mouse interfacing.
+     * Default is : true.
+     *
+     * @param emulateMouse true to enable the mouse interfacing.
+     */
+    public void setEmulateMouse(boolean emulateMouse) {
+        this.emulateMouse = emulateMouse;
+    }
+
+    /**
+     * Checks whether joystick interfacing is enabled or not.
+     * Default is : true.
+     *
+     * @return true if the joystick interfacing is enabled.
+     */
+    public boolean isUseJoyStickEvents() {
+        return useJoyStickEvents;
+    }
+
+    /**
+     * Enables joystick interfacing for a jme-game
+     *
+     * @param useJoyStickEvents true to enable the joystick interfacing.
+     */
+    public void setUseJoyStickEvents(boolean useJoyStickEvents) {
+        this.useJoyStickEvents = useJoyStickEvents;
+    }
+
+    /**
+     * Checks whether the GLThread is paused or not.
+     *
+     * @return true/false
+     */
+    public boolean isGLThreadPaused() {
+        return isGLThreadPaused;
+    }
+
+    /**
+     * Sets GL Thread paused.
+     *
+     * @param GLThreadPaused true if you want to pause the GLThread.
+     */
+    protected void setGLThreadPaused(boolean GLThreadPaused) {
+        isGLThreadPaused = GLThreadPaused;
+    }
+
+    /**
+     * Sets the listener for the completion of rendering, ie : when the GL thread holding the {@link JmeSurfaceView}
+     * joins the UI thread, after asynchronous rendering.
+     *
+     * @param onRendererCompleted an instance of the interface {@link OnRendererCompleted}.
+     */
+    public void setOnRendererCompleted(OnRendererCompleted onRendererCompleted) {
+        this.onRendererCompleted = onRendererCompleted;
+    }
+
+    /**
+     * Sets the listener that will fire when an exception is thrown.
+     *
+     * @param onExceptionThrown an instance of the interface {@link OnExceptionThrown}.
+     */
+    public void setOnExceptionThrown(OnExceptionThrown onExceptionThrown) {
+        this.onExceptionThrown = onExceptionThrown;
+    }
+
+    /**
+     * Sets the listener that will fire after initializing the game.
+     *
+     * @param onRendererStarted an instance of the interface {@link OnRendererStarted}.
+     */
+    public void setOnRendererStarted(OnRendererStarted onRendererStarted) {
+        this.onRendererStarted = onRendererStarted;
+    }
+
+    /**
+     * Sets the listener that will dispatch an event when the layout is drawn by {@link JmeSurfaceView#addGlSurfaceView()}.
+     *
+     * @param onLayoutDrawn the event to be dispatched.
+     * @see JmeSurfaceView#addGlSurfaceView()
+     */
+    public void setOnLayoutDrawn(OnLayoutDrawn onLayoutDrawn) {
+        this.onLayoutDrawn = onLayoutDrawn;
+    }
+
+    /**
+     * Gets the current device GL_ES version.
+     *
+     * @return the current gl_es version in a string format.
+     */
+    public String getGlEsVersion() {
+        return configurationInfo.getGlEsVersion();
+    }
+
+    /**
+     * Checks whether the error dialog is enabled upon encountering exceptions/errors.
+     * Default is : true.
+     *
+     * @return true if the error dialog is activated, false otherwise.
+     */
+    public boolean isShowErrorDialog() {
+        return showErrorDialog;
+    }
+
+    /**
+     * Determines whether the error dialog would be shown on encountering exceptions.
+     * Default is : true.
+     *
+     * @param showErrorDialog true to activate the error dialog, false otherwise.
+     */
+    public void setShowErrorDialog(boolean showErrorDialog) {
+        this.showErrorDialog = showErrorDialog;
+    }
+
+    /**
+     * Determines whether the app context would be destructed
+     * with the holder activity context in case of {@link DestructionPolicy#DESTROY_WHEN_FINISH} or be
+     * spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
+     * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
+     *
+     * @see JmeSurfaceView#setDestructionPolicy(DestructionPolicy)
+     */
+    public enum DestructionPolicy {
+        /**
+         * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication}).
+         */
+        DESTROY_WHEN_FINISH,
+        /**
+         * Spares the game context inside a static memory {@link GameState#legacyApplication}
+         * when the activity context is destroyed, but the app stills in the background.
+         */
+        KEEP_WHEN_FINISH
+    }
+
+    /**
+     * Used as a static memory to protect the game context from destruction by Activity#onDestroy().
+     *
+     * @see DestructionPolicy
+     * @see JmeSurfaceView#setDestructionPolicy(DestructionPolicy)
+     */
+    protected static final class GameState {
+
+        private static LegacyApplication legacyApplication;
+        private static boolean firstUpdatePassed = false;
+
+        /**
+         * Private constructor to inhibit instantiation of this class.
+         */
+        private GameState() {
+        }
+
+        /**
+         * Returns the current application state.
+         *
+         * @return game state instance, holding jME3 states (JmeContext, AssetManager, StateManager, Graphics, Sound, Input, Spatial/Nodes in place, etcetera).
+         */
+        protected static LegacyApplication getLegacyApplication() {
+            return legacyApplication;
+        }
+
+        /**
+         * Replaces the current application state.
+         *
+         * @param legacyApplication the new app instance holding the game state (including {@link AssetLoader}s, {@link AudioNode}s, {@link Spatial}s, etcetera).
+         */
+        protected static void setLegacyApplication(LegacyApplication legacyApplication) {
+            GameState.legacyApplication = legacyApplication;
+        }
+
+        /**
+         * Tests the first update flag.
+         *
+         * @return true if the firstUpdate has passed, false otherwise.
+         */
+        protected static boolean isFirstUpdatePassed() {
+            return firstUpdatePassed;
+        }
+
+        /**
+         * Adjusts the first update flag.
+         *
+         * @param firstUpdatePassed set to true to determine whether the firstUpdate has passed, false otherwise.
+         */
+        protected static void setFirstUpdatePassed(boolean firstUpdatePassed) {
+            GameState.firstUpdatePassed = firstUpdatePassed;
+        }
+    }
+
+    /**
+     * Delays the attachment surface view on the UI for the sake of initial frame pacing and splash screens,
+     * delaying the display of the game (GlSurfaceView) would lead to a substantial delay in the
+     * {@link android.opengl.GLSurfaceView.Renderer#onDrawFrame(javax.microedition.khronos.opengles.GL10)} which would
+     * delay invoking both {@link LegacyApplication#initialize()} and {@link LegacyApplication#update()}.
+     *
+     * @see JmeSurfaceView#startRenderer(int)
+     * @see com.jme3.system.android.OGLESContext#onDrawFrame(javax.microedition.khronos.opengles.GL10)
+     */
+    private class RendererThread implements Runnable {
+        /**
+         * Delays the {@link GLSurfaceView} attachment on the UI thread.
+         *
+         * @see JmeSurfaceView#startRenderer(int)
+         */
+        @Override
+        public void run() {
+            addGlSurfaceView();
+            jmeSurfaceViewLogger.log(Level.INFO, "JmeSurfaceView's joined the UI thread");
+        }
+    }
+}

+ 47 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/OnExceptionThrown.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2009-2022 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.view.surfaceview;
+
+/**
+ * An interface designed to listen for exceptions and fire an event when an exception is thrown.
+ *
+ * @author pavl_g.
+ * @see JmeSurfaceView#setOnExceptionThrown(OnExceptionThrown)
+ */
+public interface OnExceptionThrown {
+    /**
+     * Listens for a thrown exception or a thrown error.
+     *
+     * @param e the exception or the error that is throwable.
+     */
+    void onExceptionThrown(Throwable e);
+}

+ 51 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/OnLayoutDrawn.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2009-2022 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.view.surfaceview;
+
+import android.view.View;
+import com.jme3.app.LegacyApplication;
+
+/**
+ * An interface used for dispatching an event when the layout holding the {@link android.opengl.GLSurfaceView} is drawn,
+ * the event is dispatched on the user activity context thread.
+ *
+ * @author pavl_g.
+ */
+public interface OnLayoutDrawn {
+    /**
+     * Dispatched when the layout is drawn on the screen.
+     *
+     * @param legacyApplication the application instance.
+     * @param layout            the current layout.
+     */
+    void onLayoutDrawn(LegacyApplication legacyApplication, View layout);
+}

+ 53 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/OnRendererCompleted.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2009-2022 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.view.surfaceview;
+
+import com.jme3.app.LegacyApplication;
+import com.jme3.system.AppSettings;
+
+/**
+ * An interface used for invoking an event when the user delay finishes, on the first update of the game.
+ *
+ * @author pavl_g.
+ * @see JmeSurfaceView#setOnRendererCompleted(OnRendererCompleted)
+ */
+public interface OnRendererCompleted {
+    /**
+     * Invoked when the user delay finishes, on the first update of the game, the event is dispatched on the
+     * enclosing Activity context thread.
+     *
+     * @param application the current jme game instance.
+     * @param appSettings the current window settings of the running jme game.
+     * @see JmeSurfaceView#update()
+     */
+    void onRenderCompletion(LegacyApplication application, AppSettings appSettings);
+}

+ 55 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/OnRendererStarted.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2009-2022 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.view.surfaceview;
+
+import android.view.View;
+import com.jme3.app.LegacyApplication;
+
+/**
+ * An interface used for invoking an event when the application is started explicitly from {@link JmeSurfaceView#startRenderer(int)}.
+ * NB : This listener must be utilized before using {@link JmeSurfaceView#startRenderer(int)}, ie : it would be ignored if you try to use {@link JmeSurfaceView#setOnRendererStarted(OnRendererStarted)} after
+ * {@link JmeSurfaceView#startRenderer(int)}.
+ *
+ * @author pavl_g.
+ * @see JmeSurfaceView#setOnRendererStarted(OnRendererStarted)
+ */
+public interface OnRendererStarted {
+    /**
+     * Invoked when the game application is started by the {@link LegacyApplication#start()}, the event is dispatched on the
+     * holder Activity context thread.
+     *
+     * @param application the game instance.
+     * @param layout      the enclosing layout.
+     * @see JmeSurfaceView#startRenderer(int)
+     */
+    void onRenderStart(LegacyApplication application, View layout);
+}

+ 44 - 0
jme3-android/src/main/java/com/jme3/view/surfaceview/package-info.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009-2022 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.
+ */
+
+/**
+ * Holds {@link com.jme3.view.surfaceview.JmeSurfaceView} with some lifecycle interfaces.
+ * <p>
+ * This package provides the following :
+ * <p>
+ * {@link com.jme3.view.surfaceview.JmeSurfaceView} : An OpenGL android view wrapper for rendering, updating and destroying a jMonkeyEngine game.
+ * {@link com.jme3.view.surfaceview.OnRendererStarted} :  Provides a method to be invoked when a jMonkeyEngine application starts.
+ * {@link com.jme3.view.surfaceview.OnLayoutDrawn} : Provides a method to be invoked when the GLSurfaceView draws the content, before OnRendererCompleted.
+ * {@link com.jme3.view.surfaceview.OnRendererCompleted} : Provides a method to be invoked on the first update of the game.
+ * {@link com.jme3.view.surfaceview.OnExceptionThrown} : Provides a method to be invoked when an exception is thrown.
+ */
+package com.jme3.view.surfaceview;

+ 2 - 1
jme3-android/src/main/resources/com/jme3/asset/Android.cfg

@@ -5,4 +5,5 @@ LOCATOR / com.jme3.asset.plugins.AndroidLocator
 
 # Android specific loaders
 LOADER com.jme3.texture.plugins.AndroidNativeImageLoader : jpg, bmp, gif, png, jpeg
-LOADER com.jme3.audio.plugins.NativeVorbisLoader : ogg
+LOADER com.jme3.texture.plugins.AndroidBufferImageLoader : webp, heic, heif
+LOADER com.jme3.audio.plugins.OGGLoader : ogg

+ 3 - 0
jme3-awt-dialogs/build.gradle

@@ -0,0 +1,3 @@
+dependencies {
+    api project(':jme3-core')
+}

+ 98 - 0
jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTErrorDialog.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2009-2022 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.awt;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+/**
+ * Simple dialog for displaying error messages,
+ * 
+ * @author kwando
+ */
+public class AWTErrorDialog extends JDialog {
+    public static String DEFAULT_TITLE = "Error in application";
+    public static int PADDING = 8;
+    
+    /**
+     * Create a new Dialog with a title and a message.
+     *
+     * @param message the message to display
+     * @param title the title to display
+     */
+    protected AWTErrorDialog(String message, String title) {
+        setTitle(title);
+        setSize(new Dimension(600, 400));
+        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+        setLocationRelativeTo(null);   
+        
+        Container container = getContentPane();
+        container.setLayout(new BorderLayout());
+        
+        JTextArea textArea = new JTextArea();
+        textArea.setText(message);
+        textArea.setEditable(false);
+        textArea.setMargin(new Insets(PADDING, PADDING, PADDING, PADDING));
+        add(new JScrollPane(textArea), BorderLayout.CENTER);
+        
+        final JDialog dialog = this;
+        JButton button = new JButton(new AbstractAction("OK"){
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                dialog.dispose();
+            }
+        });
+        add(button, BorderLayout.SOUTH);
+    }
+    
+    protected AWTErrorDialog(String message){
+        this(message, DEFAULT_TITLE);
+    }
+    
+    /**
+     * Show a dialog with the provided message.
+     *
+     * @param message the message to display
+     */
+    public static void showDialog(String message) {
+        AWTErrorDialog dialog = new AWTErrorDialog(message);
+        dialog.setVisible(true);
+    }
+}

+ 234 - 179
jme3-desktop/src/main/java/com/jme3/app/SettingsDialog.java → jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2022 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -29,9 +29,12 @@
  * 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.app;
+package com.jme3.awt;
 
+import com.jme3.asset.AssetNotFoundException;
 import com.jme3.system.AppSettings;
+import com.jme3.system.JmeSystem;
+
 import java.awt.*;
 import java.awt.event.*;
 import java.awt.image.BufferedImage;
@@ -42,41 +45,47 @@ import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.prefs.BackingStoreException;
 import javax.swing.*;
 
 /**
- * <code>PropertiesDialog</code> provides an interface to make use of the
- * <code>GameSettings</code> class. The <code>GameSettings</code> object
- * is still created by the client application, and passed during construction.
- * 
+ * <code>SettingsDialog</code> displays a Swing dialog box to interactively
+ * configure the <code>AppSettings</code> of a desktop application before
+ * <code>start()</code> is invoked.
+ *
+ * The <code>AppSettings</code> instance to be configured is passed to the
+ * constructor.
+ *
  * @see AppSettings
  * @author Mark Powell
  * @author Eric Woroshow
  * @author Joshua Slack - reworked for proper use of GL commands.
  */
-public final class SettingsDialog extends JFrame {
+public final class AWTSettingsDialog extends JFrame {
 
     public static interface SelectionListener {
 
         public void onSelection(int selection);
     }
-    private static final Logger logger = Logger.getLogger(SettingsDialog.class.getName());
+
+    private static final Logger logger = Logger.getLogger(AWTSettingsDialog.class.getName());
     private static final long serialVersionUID = 1L;
-    public static final int NO_SELECTION = 0,
-            APPROVE_SELECTION = 1,
-            CANCEL_SELECTION = 2;
-    
+    public static final int NO_SELECTION = 0, APPROVE_SELECTION = 1, CANCEL_SELECTION = 2;
+
     // Resource bundle for i18n.
     ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
-    
-    // connection to properties file.
+
+    // the instance being configured
     private final AppSettings source;
-    
+
     // Title Image
     private URL imageFile = null;
     // Array of supported display modes
@@ -104,72 +113,133 @@ public final class SettingsDialog extends JFrame {
 
     private int minWidth = 0;
     private int minHeight = 0;
-    
+
+    public static boolean showDialog(AppSettings sourceSettings) {
+        return showDialog(sourceSettings, true);
+    }
+
+    public static boolean showDialog(AppSettings sourceSettings, boolean loadSettings) {
+        String iconPath = sourceSettings.getSettingsDialogImage();
+        final URL iconUrl = JmeSystem.class.getResource(iconPath.startsWith("/") ? iconPath : "/" + iconPath);
+        if (iconUrl == null) {
+            throw new AssetNotFoundException(sourceSettings.getSettingsDialogImage());
+        }
+        return showDialog(sourceSettings, iconUrl, loadSettings);
+    }
+
+    public static boolean showDialog(AppSettings sourceSettings, String imageFile, boolean loadSettings) {
+        return showDialog(sourceSettings, getURL(imageFile), loadSettings);
+    }
+
+    public static boolean showDialog(AppSettings sourceSettings, URL imageFile, boolean loadSettings) {
+        if (SwingUtilities.isEventDispatchThread()) {
+            throw new IllegalStateException("Cannot run from EDT");
+        }
+        if (GraphicsEnvironment.isHeadless()) {
+            throw new IllegalStateException("Cannot show dialog in headless environment");
+        }
+
+        AppSettings settings = new AppSettings(false);
+        settings.copyFrom(sourceSettings);
+
+        Object lock = new Object();
+        AtomicBoolean done = new AtomicBoolean();
+        AtomicInteger result = new AtomicInteger();
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                final SelectionListener selectionListener = new SelectionListener() {
+                    @Override
+                    public void onSelection(int selection) {
+                        synchronized (lock) {
+                            done.set(true);
+                            result.set(selection);
+                            lock.notifyAll();
+                        }
+                    }
+                };
+                AWTSettingsDialog dialog = new AWTSettingsDialog(settings, imageFile, loadSettings);
+                dialog.setSelectionListener(selectionListener);
+                dialog.showDialog();
+            }
+        });
+        synchronized (lock) {
+            while (!done.get()) {
+                try {
+                    lock.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+        }
+
+        sourceSettings.copyFrom(settings);
+
+        return result.get() == AWTSettingsDialog.APPROVE_SELECTION;
+    }
+
     /**
-     * Constructor for the <code>PropertiesDialog</code>. Creates a
-     * properties dialog initialized for the primary display.
+     * Instantiate a <code>SettingsDialog</code> for the primary display.
      *
      * @param source
-     *            the <code>AppSettings</code> object to use for working with
-     *            the properties file.
+     *            the <code>AppSettings</code> (not null)
      * @param imageFile
      *            the image file to use as the title of the dialog;
      *            <code>null</code> will result in to image being displayed
-     * @throws NullPointerException
+     * @param loadSettings
+     *            if true, copy the settings, otherwise merge them
+     * @throws IllegalArgumentException
      *             if the source is <code>null</code>
      */
-    public SettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
+    protected AWTSettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
         this(source, getURL(imageFile), loadSettings);
     }
 
     /**
-     * Constructor for the <code>PropertiesDialog</code>. Creates a
-     * properties dialog initialized for the primary display.
-     * 
+     * /** Instantiate a <code>SettingsDialog</code> for the primary display.
+     *
      * @param source
-     *            the <code>GameSettings</code> object to use for working with
-     *            the properties file.
+     *            the <code>AppSettings</code> object (not null)
      * @param imageFile
      *            the image file to use as the title of the dialog;
      *            <code>null</code> will result in to image being displayed
-     * @param loadSettings 
-     * @throws NullPointerException
+     * @param loadSettings
+     *            if true, copy the settings, otherwise merge them
+     * @throws IllegalArgumentException
      *             if the source is <code>null</code>
      */
-    public SettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
+    protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
         if (source == null) {
-            throw new NullPointerException("Settings source cannot be null");
+            throw new IllegalArgumentException("Settings source cannot be null");
         }
 
         this.source = source;
         this.imageFile = imageFile;
 
-        //setModal(true);
+        // setModal(true);
         setAlwaysOnTop(true);
         setResizable(false);
 
         AppSettings registrySettings = new AppSettings(true);
 
         String appTitle;
-        if(source.getTitle()!=null){
+        if (source.getTitle() != null) {
             appTitle = source.getTitle();
-        }else{
-           appTitle = registrySettings.getTitle();
+        } else {
+            appTitle = registrySettings.getTitle();
         }
-        
+
         minWidth = source.getMinWidth();
         minHeight = source.getMinHeight();
-        
+
         try {
             registrySettings.load(appTitle);
         } catch (BackingStoreException ex) {
-            logger.log(Level.WARNING,
-                    "Failed to load settings", ex);
+            logger.log(Level.WARNING, "Failed to load settings", ex);
         }
 
         if (loadSettings) {
             source.copyFrom(registrySettings);
-        } else if(!registrySettings.isEmpty()) {
+        } else if (!registrySettings.isEmpty()) {
             source.mergeFrom(registrySettings);
         }
 
@@ -179,17 +249,13 @@ public final class SettingsDialog extends JFrame {
         Arrays.sort(modes, new DisplayModeSorter());
 
         DisplayMode[] merged = new DisplayMode[modes.length + windowDefaults.length];
-        
+
         int wdIndex = 0;
         int dmIndex = 0;
         int mergedIndex;
-        
-        for (mergedIndex = 0;
-                mergedIndex<merged.length 
-                && (wdIndex < windowDefaults.length
-                    || dmIndex < modes.length);
-                mergedIndex++) {
-            
+
+        for (mergedIndex = 0; mergedIndex < merged.length && (wdIndex < windowDefaults.length || dmIndex < modes.length); mergedIndex++) {
+
             if (dmIndex >= modes.length) {
                 merged[mergedIndex] = windowDefaults[wdIndex++];
             } else if (wdIndex >= windowDefaults.length) {
@@ -209,13 +275,13 @@ public final class SettingsDialog extends JFrame {
                 merged[mergedIndex] = windowDefaults[wdIndex++];
             }
         }
-        
+
         if (merged.length == mergedIndex) {
             windowModes = merged;
         } else {
             windowModes = Arrays.copyOfRange(merged, 0, mergedIndex);
         }
-        
+
         createUI();
     }
 
@@ -248,9 +314,6 @@ public final class SettingsDialog extends JFrame {
         this.minHeight = minHeight;
     }
 
-    
-    
-    
     /**
      * <code>setImage</code> sets the background image of the dialog.
      * 
@@ -262,7 +325,7 @@ public final class SettingsDialog extends JFrame {
             URL file = new URL("file:" + image);
             setImage(file);
         } catch (MalformedURLException e) {
-           logger.log(Level.WARNING, "Couldn’t read from file '" + image + "'", e);
+            logger.log(Level.WARNING, "Couldn’t read from file '" + image + "'", e);
         }
     }
 
@@ -274,33 +337,27 @@ public final class SettingsDialog extends JFrame {
      */
     public void setImage(URL image) {
         icon.setIcon(new ImageIcon(image));
-        pack(); // Resize to accomodate the new image
+        pack(); // Resize to accommodate the new image
         setLocationRelativeTo(null); // put in center
     }
 
     /**
-     * <code>showDialog</code> sets this dialog as visble, and brings it to
-     * the front.
+     * <code>showDialog</code> sets this dialog as visible, and brings it to the
+     * front.
      */
     public void showDialog() {
         setLocationRelativeTo(null);
-        setVisible(true);       
+        setVisible(true);
         toFront();
     }
-   
+
     /**
      * <code>init</code> creates the components to use the dialog.
      */
     private void createUI() {
         GridBagConstraints gbc;
-        
+
         JPanel mainPanel = new JPanel(new GridBagLayout());
-        
-        try {
-            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
-        } catch (Exception e) {
-            logger.warning("Could not set native look and feel.");
-        }
 
         addWindowListener(new WindowAdapter() {
 
@@ -312,13 +369,13 @@ public final class SettingsDialog extends JFrame {
         });
 
         if (source.getIcons() != null) {
-            safeSetIconImages( Arrays.asList((BufferedImage[]) source.getIcons()) );
+            safeSetIconImages(Arrays.asList((BufferedImage[]) source.getIcons()));
         }
 
         setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
-        
+
         // The buttons...
-        JButton ok = new JButton(resourceBundle.getString("button.ok"));               
+        JButton ok = new JButton(resourceBundle.getString("button.ok"));
         JButton cancel = new JButton(resourceBundle.getString("button.cancel"));
 
         icon = new JLabel(imageFile != null ? new ImageIcon(imageFile) : null);
@@ -332,8 +389,7 @@ public final class SettingsDialog extends JFrame {
                         setUserSelection(APPROVE_SELECTION);
                         dispose();
                     }
-                }
-                else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                     setUserSelection(CANCEL_SELECTION);
                     dispose();
                 }
@@ -359,10 +415,10 @@ public final class SettingsDialog extends JFrame {
         });
         vsyncBox = new JCheckBox(resourceBundle.getString("checkbox.vsync"));
         vsyncBox.setSelected(source.isVSync());
-        
+
         gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
         gammaBox.setSelected(source.isGammaCorrection());
-        
+
         gbc = new GridBagConstraints();
         gbc.weightx = 0.5;
         gbc.gridx = 0;
@@ -372,20 +428,19 @@ public final class SettingsDialog extends JFrame {
         mainPanel.add(fullscreenBox, gbc);
         gbc = new GridBagConstraints();
         gbc.weightx = 0.5;
-      //  gbc.insets = new Insets(4, 16, 0, 4);
+        // gbc.insets = new Insets(4, 16, 0, 4);
         gbc.gridx = 2;
-      //  gbc.gridwidth = 2;
+        // gbc.gridwidth = 2;
         gbc.gridy = 1;
         gbc.anchor = GridBagConstraints.EAST;
         mainPanel.add(vsyncBox, gbc);
         gbc = new GridBagConstraints();
         gbc.weightx = 0.5;
         gbc.gridx = 3;
-        gbc.gridy = 1;       
+        gbc.gridy = 1;
         gbc.anchor = GridBagConstraints.WEST;
         mainPanel.add(gammaBox, gbc);
 
-        
         gbc = new GridBagConstraints();
         gbc.insets = new Insets(4, 4, 4, 4);
         gbc.gridx = 0;
@@ -434,7 +489,7 @@ public final class SettingsDialog extends JFrame {
         gbc.gridy = 3;
         gbc.anchor = GridBagConstraints.WEST;
         mainPanel.add(antialiasCombo, gbc);
-        
+
         // Set the button action listeners. Cancel disposes without saving, OK
         // saves.
         ok.addActionListener(new ActionListener() {
@@ -444,10 +499,14 @@ public final class SettingsDialog extends JFrame {
                 if (verifyAndSaveCurrentSelection()) {
                     setUserSelection(APPROVE_SELECTION);
                     dispose();
-                    
-                    // System.gc() should be called to prevent "X Error of failed request: RenderBadPicture (invalid Picture parameter)"
-                    // on Linux when using AWT/Swing + GLFW. 
-                    // For more info see: https://github.com/LWJGL/lwjgl3/issues/149, https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
+
+                    // System.gc() should be called to prevent "X Error of
+                    // failed request: RenderBadPicture (invalid Picture
+                    // parameter)"
+                    // on Linux when using AWT/Swing + GLFW.
+                    // For more info see:
+                    // https://github.com/LWJGL/lwjgl3/issues/149,
+                    // https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
                     System.gc();
                     System.gc();
                 }
@@ -468,7 +527,7 @@ public final class SettingsDialog extends JFrame {
         gbc.gridwidth = 2;
         gbc.gridy = 4;
         gbc.anchor = GridBagConstraints.EAST;
-        mainPanel.add(ok, gbc);        
+        mainPanel.add(ok, gbc);
         gbc = new GridBagConstraints();
         gbc.insets = new Insets(4, 16, 4, 4);
         gbc.gridx = 2;
@@ -486,35 +545,42 @@ public final class SettingsDialog extends JFrame {
         this.getContentPane().add(mainPanel);
 
         pack();
-        
+
         mainPanel.getRootPane().setDefaultButton(ok);
         SwingUtilities.invokeLater(new Runnable() {
 
             @Override
             public void run() {
-                // Fill in the combos once the window has opened so that the insets can be read.
-                // The assumption is made that the settings window and the display window will have the
-                // same insets as that is used to resize the "full screen windowed" mode appropriately.
+                // Fill in the combos once the window has opened so that the
+                // insets can be read.
+                // The assumption is made that the settings window and the
+                // display window will have the
+                // same insets as that is used to resize the "full screen
+                // windowed" mode appropriately.
                 updateResolutionChoices();
                 if (source.getWidth() != 0 && source.getHeight() != 0) {
-                    displayResCombo.setSelectedItem(source.getWidth() + " x "
-                            + source.getHeight());
+                    displayResCombo.setSelectedItem(source.getWidth() + " x " + source.getHeight());
                 } else {
-                    displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
+                    displayResCombo.setSelectedIndex(displayResCombo.getItemCount() - 1);
                 }
 
                 updateAntialiasChoices();
                 colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
             }
-        });      
-        
+        });
+
     }
 
-    /* Access JDialog.setIconImages by reflection in case we're running on JRE < 1.6 */
+    /*
+     * Access JDialog.setIconImages by reflection in case we're running on JRE <
+     * 1.6
+     */
     private void safeSetIconImages(List<? extends Image> icons) {
         try {
-            // Due to Java bug 6445278, we try to set icon on our shared owner frame first.
-            // Otherwise, our alt-tab icon will be the Java default under Windows.
+            // Due to Java bug 6445278, we try to set icon on our shared owner
+            // frame first.
+            // Otherwise, our alt-tab icon will be the Java default under
+            // Windows.
             Window owner = getOwner();
             if (owner != null) {
                 Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
@@ -532,7 +598,7 @@ public final class SettingsDialog extends JFrame {
     /**
      * <code>verifyAndSaveCurrentSelection</code> first verifies that the
      * display mode is valid for this system, and then saves the current
-     * selection as a properties.cfg file.
+     * selection to the backing store.
      * 
      * @return if the selection is valid
      */
@@ -593,7 +659,7 @@ public final class SettingsDialog extends JFrame {
         }
 
         if (valid) {
-            //use the GameSettings class to save it.
+            // use the AppSettings class to save it to backing store
             source.setWidth(width);
             source.setHeight(height);
             source.setBitsPerPixel(depth);
@@ -601,7 +667,7 @@ public final class SettingsDialog extends JFrame {
             source.setFullscreen(fullscreen);
             source.setVSync(vsync);
             source.setGammaCorrection(gamma);
-            //source.setRenderer(renderer);
+            // source.setRenderer(renderer);
             source.setSamples(multisample);
 
             String appTitle = source.getTitle();
@@ -609,13 +675,10 @@ public final class SettingsDialog extends JFrame {
             try {
                 source.save(appTitle);
             } catch (BackingStoreException ex) {
-                logger.log(Level.WARNING,
-                        "Failed to save setting changes", ex);
+                logger.log(Level.WARNING, "Failed to save setting changes", ex);
             }
         } else {
-            showError(
-                    this,
-                    resourceBundle.getString("error.unsupportedmode"));
+            showError(this, resourceBundle.getString("error.unsupportedmode"));
         }
 
         return valid;
@@ -624,7 +687,7 @@ public final class SettingsDialog extends JFrame {
     /**
      * <code>setUpChooser</code> retrieves all available display modes and
      * places them in a <code>JComboBox</code>. The resolution specified by
-     * GameSettings is used as the default value.
+     * AppSettings is used as the default value.
      * 
      * @return the combo box of display modes.
      */
@@ -670,7 +733,7 @@ public final class SettingsDialog extends JFrame {
         displayFreqCombo.setModel(new DefaultComboBoxModel<>(freqs));
         // Try to reset freq
         displayFreqCombo.setSelectedItem(displayFreq);
-        
+
         if (!displayFreqCombo.getSelectedItem().equals(displayFreq)) {
             // Cannot find saved frequency in available frequencies.
             // Choose the closest one to 60 Hz.
@@ -686,21 +749,17 @@ public final class SettingsDialog extends JFrame {
      */
     private void updateResolutionChoices() {
         if (!fullscreenBox.isSelected()) {
-            displayResCombo.setModel(new DefaultComboBoxModel<>(
-                    getWindowedResolutions(windowModes)));
+            displayResCombo.setModel(new DefaultComboBoxModel<>(getWindowedResolutions(windowModes)));
             if (displayResCombo.getItemCount() > 0) {
-                displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
+                displayResCombo.setSelectedIndex(displayResCombo.getItemCount() - 1);
             }
-            colorDepthCombo.setModel(new DefaultComboBoxModel<>(new String[]{
-                        "24 bpp", "16 bpp"}));
-            displayFreqCombo.setModel(new DefaultComboBoxModel<>(
-                    new String[]{resourceBundle.getString("refresh.na")}));
+            colorDepthCombo.setModel(new DefaultComboBoxModel<>(new String[] { "24 bpp", "16 bpp" }));
+            displayFreqCombo.setModel(new DefaultComboBoxModel<>(new String[] { resourceBundle.getString("refresh.na") }));
             displayFreqCombo.setEnabled(false);
         } else {
-            displayResCombo.setModel(new DefaultComboBoxModel<>(
-                    getResolutions(modes, Integer.MAX_VALUE, Integer.MAX_VALUE)));
+            displayResCombo.setModel(new DefaultComboBoxModel<>(getResolutions(modes, Integer.MAX_VALUE, Integer.MAX_VALUE)));
             if (displayResCombo.getItemCount() > 0) {
-                displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
+                displayResCombo.setSelectedIndex(displayResCombo.getItemCount() - 1);
             }
             displayFreqCombo.setEnabled(true);
             updateDisplayChoices();
@@ -709,10 +768,10 @@ public final class SettingsDialog extends JFrame {
 
     private void updateAntialiasChoices() {
         // maybe in the future will add support for determining this info
-        // through pbuffer
-        String[] choices = new String[]{resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x"};
+        // through PBuffer
+        String[] choices = new String[] { resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x" };
         antialiasCombo.setModel(new DefaultComboBoxModel<>(choices));
-        antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples()/2,5)]);
+        antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples() / 2, 5)]);
     }
 
     //
@@ -734,23 +793,23 @@ public final class SettingsDialog extends JFrame {
     }
 
     private static void showError(java.awt.Component parent, String message) {
-        JOptionPane.showMessageDialog(parent, message, "Error",
-                JOptionPane.ERROR_MESSAGE);
+        JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE);
     }
 
     /**
-     * Returns every unique resolution from an array of <code>DisplayMode</code>s
-     * where the resolution is greater than the configured minimums.
+     * Returns every unique resolution from an array of
+     * <code>DisplayMode</code>s where the resolution is greater than the
+     * configured minimums.
      */
     private String[] getResolutions(DisplayMode[] modes, int heightLimit, int widthLimit) {
         Insets insets = getInsets();
         heightLimit -= insets.top + insets.bottom;
         widthLimit -= insets.left + insets.right;
-        
-        ArrayList<String> resolutions = new ArrayList<String>(modes.length);
-        for (int i = 0; i < modes.length; i++) {
-            int height = modes[i].getHeight();
-            int width = modes[i].getWidth();
+
+        Set<String> resolutions = new LinkedHashSet<>(modes.length);
+        for (DisplayMode mode : modes) {
+            int height = mode.getHeight();
+            int width = mode.getWidth();
             if (width >= minWidth && height >= minHeight) {
                 if (height >= heightLimit) {
                     height = heightLimit;
@@ -758,34 +817,31 @@ public final class SettingsDialog extends JFrame {
                 if (width >= widthLimit) {
                     width = widthLimit;
                 }
-                
+
                 String res = width + " x " + height;
-                if (!resolutions.contains(res)) {
-                    resolutions.add(res);
-                }
+                resolutions.add(res);
             }
         }
 
-        String[] res = new String[resolutions.size()];
-        resolutions.toArray(res);
-        return res;
+        return resolutions.toArray(new String[0]);
     }
-    
+
     /**
-     * Returns every unique resolution from an array of <code>DisplayMode</code>s
-     * where the resolution is greater than the configured minimums and the height
-     * is less than the current screen resolution.
+     * Returns every unique resolution from an array of
+     * <code>DisplayMode</code>s where the resolution is greater than the
+     * configured minimums and the height is less than the current screen
+     * resolution.
      */
     private String[] getWindowedResolutions(DisplayMode[] modes) {
         int maxHeight = 0;
         int maxWidth = 0;
-        
-        for (int i = 0; i < modes.length; i++) {
-            if (maxHeight < modes[i].getHeight()) {
-                maxHeight = modes[i].getHeight();
+
+        for (DisplayMode mode : modes) {
+            if (maxHeight < mode.getHeight()) {
+                maxHeight = mode.getHeight();
             }
-            if (maxWidth < modes[i].getWidth()) {
-                maxWidth = modes[i].getWidth();
+            if (maxWidth < mode.getWidth()) {
+                maxWidth = mode.getWidth();
             }
         }
 
@@ -796,79 +852,78 @@ public final class SettingsDialog extends JFrame {
      * Returns every possible bit depth for the given resolution.
      */
     private static String[] getDepths(String resolution, DisplayMode[] modes) {
-        ArrayList<String> depths = new ArrayList<String>(4);
-        for (int i = 0; i < modes.length; i++) {
+        List<String> depths = new ArrayList<>(4);
+        for (DisplayMode mode : modes) {
+            int bitDepth = mode.getBitDepth();
+            if (bitDepth == DisplayMode.BIT_DEPTH_MULTI) {
+                continue;
+            }
             // Filter out all bit depths lower than 16 - Java incorrectly
             // reports
             // them as valid depths though the monitor does not support them
-            if (modes[i].getBitDepth() < 16 && modes[i].getBitDepth() > 0) {
+            if (bitDepth < 16 && bitDepth > 0) {
                 continue;
             }
-
-            String res = modes[i].getWidth() + " x " + modes[i].getHeight();
-            String depth = modes[i].getBitDepth() + " bpp";
-            if (res.equals(resolution) && !depths.contains(depth)) {
+            String res = mode.getWidth() + " x " + mode.getHeight();
+            if (!res.equals(resolution)) {
+                continue;
+            }
+            String depth = bitDepth + " bpp";
+            if (!depths.contains(depth)) {
                 depths.add(depth);
             }
         }
 
-        if (depths.size() == 1 && depths.contains("-1 bpp")) {
-            // add some default depths, possible system is multi-depth supporting
-            depths.clear();
+        if (depths.isEmpty()) {
+            // add some default depths, possible system is multi-depth
+            // supporting
             depths.add("24 bpp");
         }
 
-        String[] res = new String[depths.size()];
-        depths.toArray(res);
-        return res;
+        return depths.toArray(new String[0]);
     }
 
     /**
      * Returns every possible refresh rate for the given resolution.
      */
-    private static String[] getFrequencies(String resolution,
-            DisplayMode[] modes) {
-        ArrayList<String> freqs = new ArrayList<String>(4);
-        for (int i = 0; i < modes.length; i++) {
-            String res = modes[i].getWidth() + " x " + modes[i].getHeight();
+    private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
+        List<String> freqs = new ArrayList<>(4);
+        for (DisplayMode mode : modes) {
+            String res = mode.getWidth() + " x " + mode.getHeight();
             String freq;
-            if (modes[i].getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) {
+            if (mode.getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) {
                 freq = "???";
             } else {
-                freq = modes[i].getRefreshRate() + " Hz";
+                freq = mode.getRefreshRate() + " Hz";
             }
-
             if (res.equals(resolution) && !freqs.contains(freq)) {
                 freqs.add(freq);
             }
         }
 
-        String[] res = new String[freqs.size()];
-        freqs.toArray(res);
-        return res;
+        return freqs.toArray(new String[0]);
     }
-    
+
     /**
      * Chooses the closest frequency to 60 Hz.
      * 
      * @param resolution
      * @param modes
-     * @return 
+     * @return
      */
     private static String getBestFrequency(String resolution, DisplayMode[] modes) {
         int closest = Integer.MAX_VALUE;
         int desired = 60;
-        for (int i = 0; i < modes.length; i++) {
-            String res = modes[i].getWidth() + " x " + modes[i].getHeight();
-            int freq = modes[i].getRefreshRate();
+        for (DisplayMode mode : modes) {
+            String res = mode.getWidth() + " x " + mode.getHeight();
+            int freq = mode.getRefreshRate();
             if (freq != DisplayMode.REFRESH_RATE_UNKNOWN && res.equals(resolution)) {
-                if (Math.abs(freq - desired) < 
-                    Math.abs(closest - desired)) {
-                    closest = modes[i].getRefreshRate();
+                if (Math.abs(freq - desired) < Math.abs(closest - desired)) {
+                    closest = mode.getRefreshRate();
                 }
             }
         }
-        
+
         if (closest != Integer.MAX_VALUE) {
             return closest + " Hz";
         } else {
@@ -877,8 +932,8 @@ public final class SettingsDialog extends JFrame {
     }
 
     /**
-     * Utility class for sorting <code>DisplayMode</code>s. Sorts by
-     * resolution, then bit depth, and then finally refresh rate.
+     * Utility class for sorting <code>DisplayMode</code>s. Sorts by resolution,
+     * then bit depth, and then finally refresh rate.
      */
     private class DisplayModeSorter implements Comparator<DisplayMode> {
 

+ 48 - 0
jme3-awt-dialogs/src/main/java/com/jme3/system/JmeDialogsFactoryImpl.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2009-2022 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;
+
+import com.jme3.awt.AWTErrorDialog;
+import com.jme3.awt.AWTSettingsDialog;
+
+public class JmeDialogsFactoryImpl implements JmeDialogsFactory {
+    
+    public boolean showSettingsDialog(AppSettings settings, boolean loadFromRegistry){
+        return AWTSettingsDialog.showDialog(settings,loadFromRegistry);
+    }
+
+    public void showErrorDialog(String message){
+        AWTErrorDialog.showDialog(message);
+    }
+    
+}

+ 6 - 6
jme3-core/build.gradle

@@ -1,7 +1,3 @@
-if (!hasProperty('mainClass')) {
-    ext.mainClass = ''
-}
-
 sourceSets {
     main {
         java {
@@ -14,11 +10,15 @@ sourceSets {
         java {
             srcDir 'src/test/java'
         }
+
+        System.setProperty "java.awt.headless", "true"
     }
 }
 
 dependencies {
-    testCompile project(':jme3-testdata')
+    testRuntimeOnly project(':jme3-testdata')
+    testImplementation project(':jme3-desktop')
+    testRuntimeOnly project(':jme3-plugins')
 }
 
 task updateVersionPropertiesFile {
@@ -44,4 +44,4 @@ task updateVersionPropertiesFile {
     }
 }
 
-processResources.dependsOn updateVersionPropertiesFile
+processResources.dependsOn updateVersionPropertiesFile

+ 2 - 2
jme3-core/src/main/java/checkers/quals/DefaultLocation.java

@@ -15,8 +15,8 @@ public enum DefaultLocation {
     ALL_EXCEPT_LOCALS,
 
     /** Apply default annotations to unannotated upper bounds:  both
-     * explicit ones in <tt>extends</tt> clauses, and implicit upper bounds
-     * when no explicit <tt>extends</tt> or <tt>super</tt> clause is
+     * explicit ones in <code>extends</code> clauses, and implicit upper bounds
+     * when no explicit <code>extends</code> or <code>super</code> clause is
      * present. */
     // Especially useful for parameterized classes that provide a lot of
     // static methods with the same generic parameters as the class.

+ 2 - 0
jme3-core/src/main/java/checkers/quals/DefaultQualifier.java

@@ -36,6 +36,8 @@ public @interface DefaultQualifier {
      * To prevent affecting other type systems, always specify an annotation
      * in your own type hierarchy.  (For example, do not set
      * "checkers.quals.Unqualified" as the default.)
+     * 
+     * @return the name of the default annotation
      */
     String value();
 

+ 3 - 3
jme3-core/src/main/java/checkers/quals/DefaultQualifiers.java

@@ -16,13 +16,13 @@ import java.lang.annotation.Target;
  *
  * Example:
  * <!-- &nbsp; is a hack that prevents @ from being the first character on the line, which confuses Javadoc -->
- * <code><pre>
+ * <pre>
  * &nbsp; @DefaultQualifiers({
  * &nbsp;     @DefaultQualifier("NonNull"),
  * &nbsp;     @DefaultQualifier(value = "Interned", locations = ALL_EXCEPT_LOCALS),
  * &nbsp;     @DefaultQualifier("Tainted")
  * &nbsp; })
- * </pre></code>
+ * </pre>
  *
  * @see DefaultQualifier
  */
@@ -30,6 +30,6 @@ import java.lang.annotation.Target;
 @Retention(RetentionPolicy.RUNTIME)
 @Target({CONSTRUCTOR, METHOD, FIELD, LOCAL_VARIABLE, PARAMETER, TYPE})
 public @interface DefaultQualifiers {
-    /** The default qualifier settings */
+    /** @return the default qualifier settings */
     DefaultQualifier[] value() default { };
 }

+ 2 - 2
jme3-core/src/main/java/checkers/quals/Dependent.java

@@ -26,12 +26,12 @@ import java.lang.annotation.RetentionPolicy;
 public @interface Dependent {
 
     /**
-     * The class of the refined qualifier to be applied.
+     * @return the class of the refined qualifier to be applied.
      */
     Class<? extends Annotation> result();
 
     /**
-     * The qualifier class of the receiver that causes the {@code result}
+     * @return the qualifier class of the receiver that causes the {@code result}
      * qualifier to be applied.
      */
     Class<? extends Annotation> when();

+ 1 - 1
jme3-core/src/main/java/checkers/quals/SubtypeOf.java

@@ -54,6 +54,6 @@ import java.lang.annotation.*;
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.ANNOTATION_TYPE)
 public @interface SubtypeOf {
-    /** An array of the supertype qualifiers of the annotated qualifier **/
+    /** @return supertype qualifiers of the annotated qualifier **/
     Class<? extends Annotation>[] value();
 }

+ 2 - 0
jme3-core/src/main/java/checkers/quals/Unused.java

@@ -38,6 +38,8 @@ public @interface Unused {
     /**
      * The field that is annotated with @Unused may not be accessed via a
      * receiver that is annotated with the "when" annotation.
+     *
+     * @return the annotation class
      */
     Class<? extends Annotation> when();
 }

+ 65 - 3
jme3-core/src/main/java/com/jme3/anim/AnimClip.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,6 +38,8 @@ import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
+ * A named set of animation tracks that can be played in synchrony.
+ *
  * Created by Nehon on 20/12/2017.
  */
 public class AnimClip implements JmeCloneable, Savable {
@@ -47,13 +49,27 @@ public class AnimClip implements JmeCloneable, Savable {
 
     private AnimTrack[] tracks;
 
+    /**
+     * No-argument constructor needed by SavableClassUtil.
+     */
     protected AnimClip() {
     }
 
+    /**
+     * Instantiate a zero-length clip with the specified name.
+     *
+     * @param name desired name for the new clip
+     */
     public AnimClip(String name) {
         this.name = name;
     }
 
+    /**
+     * Replace all tracks in this clip. This method may increase the clip's
+     * length, but it will never reduce it.
+     *
+     * @param tracks the tracks to use (alias created)
+     */
     public void setTracks(AnimTrack[] tracks) {
         this.tracks = tracks;
         for (AnimTrack track : tracks) {
@@ -63,20 +79,38 @@ public class AnimClip implements JmeCloneable, Savable {
         }
     }
 
+    /**
+     * Determine the name of this clip.
+     *
+     * @return the name
+     */
     public String getName() {
         return name;
     }
 
-
+    /**
+     * Determine the duration of this clip.
+     *
+     * @return the duration (in seconds)
+     */
     public double getLength() {
         return length;
     }
 
-
+    /**
+     * Access all the tracks in this clip.
+     *
+     * @return the pre-existing array
+     */
     public AnimTrack[] getTracks() {
         return tracks;
     }
 
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
     @Override
     public Object jmeClone() {
         try {
@@ -86,6 +120,15 @@ public class AnimClip implements JmeCloneable, Savable {
         }
     }
 
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned clip into a deep-cloned one, using the specified Cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the Cloner that's cloning this clip (not null)
+     * @param original the instance from which this clip was shallow-cloned (not
+     * null, unaffected)
+     */
     @Override
     public void cloneFields(Cloner cloner, Object original) {
         AnimTrack[] newTracks = new AnimTrack[tracks.length];
@@ -95,11 +138,23 @@ public class AnimClip implements JmeCloneable, Savable {
         this.tracks = newTracks;
     }
 
+    /**
+     * Represent this clip as a String.
+     *
+     * @return a descriptive string of text (not null, not empty)
+     */
     @Override
     public String toString() {
         return "Clip " + name + ", " + length + 's';
     }
 
+    /**
+     * Serialize this clip to the specified exporter, for example when saving to
+     * a J3O file.
+     *
+     * @param ex the exporter to write to (not null)
+     * @throws IOException from the exporter
+     */
     @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
@@ -108,6 +163,13 @@ public class AnimClip implements JmeCloneable, Savable {
 
     }
 
+    /**
+     * De-serialize this clip from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param im the importer to read from (not null)
+     * @throws IOException from the importer
+     */
     @Override
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);

+ 195 - 130
jme3-core/src/main/java/com/jme3/anim/AnimComposer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2022 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -42,19 +42,16 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
-
 import java.io.IOException;
 import java.util.*;
 
 /**
  * AnimComposer is a Spatial control that allows manipulation of
  * {@link Armature armature} (skeletal) animation.
- * 
+ *
  * @author Nehon
  */
 public class AnimComposer extends AbstractControl {
-
     /**
      * The name of the default layer.
      */
@@ -63,15 +60,18 @@ public class AnimComposer extends AbstractControl {
 
     private Map<String, Action> actions = new HashMap<>();
     private float globalSpeed = 1f;
-    private Map<String, Layer> layers = new LinkedHashMap<>();
+    private Map<String, AnimLayer> layers = new LinkedHashMap<>(4);
 
+    /**
+     * Instantiate a composer with a single layer, no actions, and no clips.
+     */
     public AnimComposer() {
-        layers.put(DEFAULT_LAYER, new Layer(this));
+        layers.put(DEFAULT_LAYER, new AnimLayer(DEFAULT_LAYER, null));
     }
 
     /**
      * Tells if an animation is contained in the list of animations.
-     * 
+     *
      * @param name The name of the animation.
      * @return true, if the named animation is in the list of animations.
      */
@@ -115,104 +115,107 @@ public class AnimComposer extends AbstractControl {
     }
 
     /**
-     * Run an action on the default layer.
-     * 
+     * Run an action on the default layer. By default action will loop.
+     *
      * @param name The name of the action to run.
      * @return The action corresponding to the given name.
      */
     public Action setCurrentAction(String name) {
         return setCurrentAction(name, DEFAULT_LAYER);
     }
-    
+
     /**
-     * Run an action on specified layer.
-     * 
+     * Run an action on specified layer. By default action will loop.
+     *
      * @param actionName The name of the action to run.
      * @param layerName The layer on which action should run.
      * @return The action corresponding to the given name.
      */
     public Action setCurrentAction(String actionName, String layerName) {
-        Layer l = layers.get(layerName);
-        if (l == null) {
-            throw new IllegalArgumentException("Unknown layer " + layerName);
-        }
-        
+        return setCurrentAction(actionName, layerName, true);
+    }
+
+    /**
+     * Run an action on specified layer.
+     *
+     * @param actionName The name of the action to run.
+     * @param layerName The layer on which action should run.
+     * @param loop True if the action must loop.
+     * @return The action corresponding to the given name.
+     */
+    public Action setCurrentAction(String actionName, String layerName, boolean loop) {
+        AnimLayer l = getLayer(layerName);
         Action currentAction = action(actionName);
-        l.time = 0;
-        l.currentAction = currentAction;
+        l.setCurrentAction(actionName, currentAction, loop);
+
         return currentAction;
     }
-    
+
     /**
      * Return the current action on the default layer.
-     * 
-     * @return  The action corresponding to the given name.
+     *
+     * @return The action corresponding to the given name.
      */
     public Action getCurrentAction() {
         return getCurrentAction(DEFAULT_LAYER);
     }
-    
+
     /**
      * Return current action on specified layer.
-     * 
+     *
      * @param layerName The layer on which action should run.
      * @return The action corresponding to the given name.
      */
     public Action getCurrentAction(String layerName) {
-        Layer l = layers.get(layerName);
-        if (l == null) {
-            throw new IllegalArgumentException("Unknown layer " + layerName);
-        }
-        
-        return l.currentAction;
+        AnimLayer l = getLayer(layerName);
+        Action result = l.getCurrentAction();
+
+        return result;
     }
-    
+
     /**
      * Remove current action on default layer.
      */
     public void removeCurrentAction() {
         removeCurrentAction(DEFAULT_LAYER);
     }
-    
+
     /**
      * Remove current action on specified layer.
      *
      * @param layerName The name of the layer we want to remove its action.
      */
     public void removeCurrentAction(String layerName) {
-        Layer l = layers.get(layerName);
-        if (l == null) {
-            throw new IllegalArgumentException("Unknown layer " + layerName);
-        }
-        
-        l.time = 0;
-        l.currentAction = null;
+        AnimLayer l = getLayer(layerName);
+        l.setCurrentAction(null);
     }
-    
+
     /**
      * Returns current time of the default layer.
-     * 
+     *
      * @return The current time.
      */
     public double getTime() {
         return getTime(DEFAULT_LAYER);
     }
-    
+
     /**
      * Returns current time of the specified layer.
-     * 
+     *
      * @param layerName The layer from which to get the time.
+     * @return the time (in seconds)
      */
     public double getTime(String layerName) {
-        Layer l = layers.get(layerName);
-        if (l == null) {
-            throw new IllegalArgumentException("Unknown layer " + layerName);
-        }
-        return l.time;
+        AnimLayer l = getLayer(layerName);
+        double result = l.getTime();
+
+        return result;
     }
-    
+
     /**
      * Sets current time on the default layer.
+     *
+     * @param time the desired time (in seconds)
      */
     public void setTime(double time) {
         setTime(DEFAULT_LAYER, time);
@@ -220,25 +223,21 @@ public class AnimComposer extends AbstractControl {
 
     /**
      * Sets current time on the specified layer.
+     *
+     * @param layerName the name of the Layer to modify
+     * @param time the desired time (in seconds)
      */
     public void setTime(String layerName, double time) {
-        Layer l = layers.get(layerName);
-        if (l == null) {
-            throw new IllegalArgumentException("Unknown layer " + layerName);
-        }
-        if (l.currentAction == null) {
+        AnimLayer l = getLayer(layerName);
+        if (l.getCurrentAction() == null) {
             throw new RuntimeException("There is no action running in layer " + layerName);
         }
-        double length = l.currentAction.getLength();
-        if (time >= 0) {
-            l.time = time % length;
-        } else {
-            l.time = time % length + length;
-        }
+
+        l.setTime(time);
     }
 
     /**
-     * 
+     *
      * @param name The name of the action to return.
      * @return The action registered with specified name. It will make a new action if there isn't any.
      * @see #makeAction(java.lang.String)
@@ -251,29 +250,29 @@ public class AnimComposer extends AbstractControl {
         }
         return action;
     }
-    
+
     /**
-     * 
+     *
      * @param name The name of the action to return.
      * @return The action registered with specified name or null if nothing is registered.
      */
-    public Action getAction(String name){
+    public Action getAction(String name) {
         return actions.get(name);
     }
-    
+
     /**
      * Register given action with specified name.
-     * 
+     *
      * @param name The name of the action.
      * @param action The action to add.
      */
-    public void addAction(String name, Action action){
+    public void addAction(String name, Action action) {
         actions.put(name, action);
     }
 
     /**
      * Create a new ClipAction with specified clip name.
-     * 
+     *
      * @param name The name of the clip.
      * @return a new action
      * @throws IllegalArgumentException if clip with specified name not found.
@@ -287,17 +286,17 @@ public class AnimComposer extends AbstractControl {
         action = new ClipAction(clip);
         return action;
     }
-    
+
     /**
      * Tells if an action is contained in the list of actions.
-     * 
+     *
      * @param name The name of the action.
      * @return true, if the named action is in the list of actions.
      */
     public boolean hasAction(String name) {
         return actions.containsKey(name);
     }
-    
+
     /**
      * Remove specified action.
      *
@@ -308,9 +307,14 @@ public class AnimComposer extends AbstractControl {
         return actions.remove(name);
     }
 
+    /**
+     * Add a layer to this composer.
+     *
+     * @param name the desired name for the new layer
+     * @param mask the desired mask for the new layer (alias created)
+     */
     public void makeLayer(String name, AnimationMask mask) {
-        Layer l = new Layer(this);
-        l.mask = mask;
+        AnimLayer l = new AnimLayer(name, mask);
         layers.put(name, l);
     }
 
@@ -326,6 +330,10 @@ public class AnimComposer extends AbstractControl {
     /**
      * Creates an action that will interpolate over an entire sequence
      * of tweens in order.
+     *
+     * @param name a name for the new Action
+     * @param tweens the desired sequence of tweens
+     * @return a new instance
      */
     public BaseAction actionSequence(String name, Tween... tweens) {
         BaseAction action = new BaseAction(Tweens.sequence(tweens));
@@ -336,6 +344,11 @@ public class AnimComposer extends AbstractControl {
     /**
      * Creates an action that blends the named clips using the given blend
      * space.
+     *
+     * @param name a name for the new Action
+     * @param blendSpace how to blend the clips (not null, alias created)
+     * @param clips the names of the clips to be used (not null)
+     * @return a new instance
      */
     public BlendAction actionBlended(String name, BlendSpace blendSpace, String... clips) {
         BlendableAction[] acts = new BlendableAction[clips.length];
@@ -348,10 +361,12 @@ public class AnimComposer extends AbstractControl {
         return action;
     }
 
+    /**
+     * Reset all layers to t=0 with no current action.
+     */
     public void reset() {
-        for (Layer layer : layers.values()) {
-            layer.currentAction = null;
-            layer.time = 0;
+        for (AnimLayer layer : layers.values()) {
+            layer.setCurrentAction(null);
         }
     }
 
@@ -376,38 +391,101 @@ public class AnimComposer extends AbstractControl {
         return Collections.unmodifiableSet(animClipMap.keySet());
     }
 
+    /**
+     * used internally
+     *
+     * @param tpf time per frame (in seconds)
+     */
     @Override
     protected void controlUpdate(float tpf) {
-        for (Layer layer : layers.values()) {
-            Action currentAction = layer.currentAction;
-            if (currentAction == null) {
-                continue;
-            }
-            layer.advance(tpf);
-
-            currentAction.setMask(layer.mask);
-            boolean running = currentAction.interpolate(layer.time);
-            currentAction.setMask(null);
-
-            if (!running) {
-                layer.time = 0;
-            }
+        for (AnimLayer layer : layers.values()) {
+            layer.update(tpf, globalSpeed);
         }
     }
 
+    /**
+     * used internally
+     *
+     * @param rm the RenderManager rendering the controlled Spatial (not null)
+     * @param vp the ViewPort being rendered (not null)
+     */
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
 
     }
 
+    /**
+     * Determine the global speed applied to all layers.
+     *
+     * @return the speed factor (1=normal speed)
+     */
     public float getGlobalSpeed() {
         return globalSpeed;
     }
 
+    /**
+     * Alter the global speed applied to all layers.
+     *
+     * @param globalSpeed the desired speed factor (1=normal speed, default=1)
+     */
     public void setGlobalSpeed(float globalSpeed) {
         this.globalSpeed = globalSpeed;
     }
 
+    /**
+     * Provides access to the named layer.
+     *
+     * @param layerName the name of the layer to access
+     * @return the pre-existing instance
+     */
+    public AnimLayer getLayer(String layerName) {
+        AnimLayer result = layers.get(layerName);
+        if (result == null) {
+            throw new IllegalArgumentException("Unknown layer " + layerName);
+        }
+        return result;
+    }
+
+    /**
+     * Access the manager of the named layer.
+     *
+     * @param layerName the name of the layer to access
+     * @return the current manager (typically an AnimEvent) or null for none
+     */
+    public Object getLayerManager(String layerName) {
+        AnimLayer layer = getLayer(layerName);
+        Object result = layer.getManager();
+
+        return result;
+    }
+
+    /**
+     * Enumerates the names of all layers.
+     *
+     * @return an unmodifiable set of names
+     */
+    public Set<String> getLayerNames() {
+        Set<String> result = Collections.unmodifiableSet(layers.keySet());
+        return result;
+    }
+
+    /**
+     * Assign a manager to the named layer.
+     *
+     * @param layerName the name of the layer to modify
+     * @param manager the desired manager (typically an AnimEvent) or null for
+     * none
+     */
+    public void setLayerManager(String layerName, Object manager) {
+        AnimLayer layer = getLayer(layerName);
+        layer.setManager(manager);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
     @Override
     public Object jmeClone() {
         try {
@@ -418,6 +496,15 @@ public class AnimComposer extends AbstractControl {
         }
     }
 
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned composer into a deep-cloned one, using the specified
+     * Cloner and original to resolve copied fields.
+     *
+     * @param cloner the Cloner that's cloning this composer (not null)
+     * @param original the instance from which this composer was shallow-cloned
+     * (not null, unaffected)
+     */
     @Override
     public void cloneFields(Cloner cloner, Object original) {
         super.cloneFields(cloner, original);
@@ -432,7 +519,7 @@ public class AnimComposer extends AbstractControl {
         actions = act;
         animClipMap = clips;
 
-        Map<String, Layer> newLayers = new LinkedHashMap<>();
+        Map<String, AnimLayer> newLayers = new LinkedHashMap<>();
         for (String key : layers.keySet()) {
             newLayers.put(key, cloner.clone(layers.get(key)));
         }
@@ -441,6 +528,13 @@ public class AnimComposer extends AbstractControl {
 
     }
 
+    /**
+     * De-serialize this composer from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param im the importer to use (not null)
+     * @throws IOException from the importer
+     */
     @Override
     @SuppressWarnings("unchecked")
     public void read(JmeImporter im) throws IOException {
@@ -448,51 +542,22 @@ public class AnimComposer extends AbstractControl {
         InputCapsule ic = im.getCapsule(this);
         animClipMap = (Map<String, AnimClip>) ic.readStringSavableMap("animClipMap", new HashMap<String, AnimClip>());
         globalSpeed = ic.readFloat("globalSpeed", 1f);
+        layers = (Map<String, AnimLayer>) ic.readStringSavableMap("layers", new HashMap<String, AnimLayer>());
     }
 
+    /**
+     * Serialize this composer to the specified exporter, for example when
+     * saving to a J3O file.
+     *
+     * @param ex the exporter to use (not null)
+     * @throws IOException from the exporter
+     */
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap<String, AnimClip>());
         oc.write(globalSpeed, "globalSpeed", 1f);
-    }
-
-    private static class Layer implements JmeCloneable {
-        private AnimComposer ac;
-        private Action currentAction;
-        private AnimationMask mask;
-        private float weight;
-        private double time;
-
-        public Layer(AnimComposer ac) {
-            this.ac = ac;
-        }
-        
-        public void advance(float tpf) {
-            time += tpf * currentAction.getSpeed() * ac.globalSpeed;
-            // make sure negative time is in [0, length] range
-            if (time < 0) {
-                double length = currentAction.getLength();
-                time = (time % length + length) % length;
-            }
-
-        }
-
-        @Override
-        public Object jmeClone() {
-            try {
-                Layer clone = (Layer) super.clone();
-                return clone;
-            } catch (CloneNotSupportedException ex) {
-                throw new AssertionError();
-            }
-        }
-
-        @Override
-        public void cloneFields(Cloner cloner, Object original) {
-            ac = cloner.clone(ac);
-            currentAction = null;
-        }
+        oc.writeStringSavableMap(layers, "layers", new HashMap<String, AnimLayer>());
     }
 }

+ 421 - 0
jme3-core/src/main/java/com/jme3/anim/AnimFactory.java

@@ -0,0 +1,421 @@
+/*
+ * Copyright (c) 2009-2021 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.anim;
+
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A convenience class to smoothly animate a Spatial using translation,
+ * rotation, and scaling.
+ *
+ * Add keyframes for translation, rotation, and scaling. Invoking
+ * {@link #buildAnimation(com.jme3.anim.util.HasLocalTransform)} will then
+ * generate an AnimClip that interpolates among the keyframes.
+ *
+ * By default, the first keyframe (index=0) has an identity Transform. You can
+ * override this by replacing the first keyframe with different Transform.
+ *
+ * For a loop animation, make sure the final transform matches the starting one.
+ * Because of a heuristic used by
+ * {@link com.jme3.math.Quaternion#slerp(com.jme3.math.Quaternion, com.jme3.math.Quaternion, float)},
+ * it's possible for
+ * {@link #buildAnimation(com.jme3.anim.util.HasLocalTransform)} to negate the
+ * final rotation. To prevent an unwanted rotation at the end of the loop, you
+ * may need to add intermediate rotation keyframes.
+ *
+ * Inspired by Nehon's {@link com.jme3.animation.AnimationFactory}.
+ */
+public class AnimFactory {
+
+    /**
+     * clip duration (in seconds)
+     */
+    final private float duration;
+    /**
+     * frame/sample rate for the clip (in frames per second)
+     */
+    final private float fps;
+    /**
+     * rotations that define the clip
+     */
+    final private Map<Float, Quaternion> rotations = new TreeMap<>();
+    /**
+     * scales that define the clip
+     */
+    final private Map<Float, Vector3f> scales = new TreeMap<>();
+    /**
+     * translations that define the clip
+     */
+    final private Map<Float, Vector3f> translations = new TreeMap<>();
+    /**
+     * name for the resulting clip
+     */
+    final private String name;
+
+    /**
+     * Instantiate an AnimFactory with an identity transform at t=0.
+     *
+     * @param duration the duration for the clip (in seconds, &gt;0)
+     * @param name the name for the resulting clip
+     * @param fps the frame rate for the clip (in frames per second, &gt;0)
+     */
+    public AnimFactory(float duration, String name, float fps) {
+        if (!(duration > 0f)) {
+            throw new IllegalArgumentException("duration must be positive");
+        }
+        if (!(fps > 0f)) {
+            throw new IllegalArgumentException("FPS must be positive");
+        }
+
+        this.name = name;
+        this.duration = duration;
+        this.fps = fps;
+        /*
+         * Add the initial Transform.
+         */
+        Transform transform = new Transform();
+        translations.put(0f, transform.getTranslation());
+        rotations.put(0f, transform.getRotation());
+        scales.put(0f, transform.getScale());
+    }
+
+    /**
+     * Add a keyframe for the specified rotation at the specified index.
+     *
+     * @param keyFrameIndex the keyframe in which full rotation should be
+     * achieved (&ge;0)
+     * @param rotation the local rotation to apply to the target (not null,
+     * non-zero norm, unaffected)
+     */
+    public void addKeyFrameRotation(int keyFrameIndex, Quaternion rotation) {
+        float animationTime = keyFrameIndex / fps;
+        addTimeRotation(animationTime, rotation);
+    }
+
+    /**
+     * Add a keyframe for the specified scaling at the specified index.
+     *
+     * @param keyFrameIndex the keyframe in which full scaling should be
+     * achieved (&ge;0)
+     * @param scale the local scaling to apply to the target (not null,
+     * unaffected)
+     */
+    public void addKeyFrameScale(int keyFrameIndex, Vector3f scale) {
+        float animationTime = keyFrameIndex / fps;
+        addTimeScale(animationTime, scale);
+    }
+
+    /**
+     * Add a keyframe for the specified Transform at the specified index.
+     *
+     * @param keyFrameIndex the keyframe in which the full Transform should be
+     * achieved (&ge;0)
+     * @param transform the local Transform to apply to the target (not null,
+     * unaffected)
+     */
+    public void addKeyFrameTransform(int keyFrameIndex, Transform transform) {
+        float time = keyFrameIndex / fps;
+        addTimeTransform(time, transform);
+    }
+
+    /**
+     * Add a keyframe for the specified translation at the specified index.
+     *
+     * @param keyFrameIndex the keyframe in which full translation should be
+     * achieved (&ge;0)
+     * @param offset the local translation to apply to the target (not null,
+     * unaffected)
+     */
+    public void addKeyFrameTranslation(int keyFrameIndex, Vector3f offset) {
+        float time = keyFrameIndex / fps;
+        addTimeTranslation(time, offset);
+    }
+
+    /**
+     * Add a keyframe for the specified rotation at the specified time.
+     *
+     * @param time the animation time when full rotation should be achieved
+     * (&ge;0, &le;duration)
+     * @param rotation the local rotation to apply to the target (not null,
+     * non-zero norm, unaffected)
+     */
+    public void addTimeRotation(float time, Quaternion rotation) {
+        if (!(time >= 0f && time <= duration)) {
+            throw new IllegalArgumentException("animation time out of range");
+        }
+        float norm = rotation.norm();
+        if (norm == 0f) {
+            throw new IllegalArgumentException("rotation cannot have norm=0");
+        }
+
+        float normalizingFactor = 1f / FastMath.sqrt(norm);
+        Quaternion normalized = rotation.mult(normalizingFactor);
+        rotations.put(time, normalized);
+    }
+
+    /**
+     * Add a keyframe for the specified rotation at the specified time, based on
+     * Tait-Bryan angles. Note that this is NOT equivalent to
+     * {@link com.jme3.animation.AnimationFactory#addTimeRotationAngles(float, float, float, float)}.
+     *
+     * @param time the animation time when full rotation should be achieved
+     * (&ge;0, &le;duration)
+     * @param xAngle the X angle (in radians)
+     * @param yAngle the Y angle (in radians)
+     * @param zAngle the Z angle (in radians)
+     */
+    public void addTimeRotation(float time, float xAngle, float yAngle,
+            float zAngle) {
+        if (!(time >= 0f && time <= duration)) {
+            throw new IllegalArgumentException("animation time out of range");
+        }
+
+        Quaternion quat = new Quaternion().fromAngles(xAngle, yAngle, zAngle);
+        rotations.put(time, quat);
+    }
+
+    /**
+     * Add a keyframe for the specified scale at the specified time.
+     *
+     * @param time the animation time when full scaling should be achieved
+     * (&ge;0, &le;duration)
+     * @param scale the local scaling to apply to the target (not null,
+     * unaffected)
+     */
+    public void addTimeScale(float time, Vector3f scale) {
+        if (!(time >= 0f && time <= duration)) {
+            throw new IllegalArgumentException("animation time out of range");
+        }
+
+        Vector3f clone = scale.clone();
+        scales.put(time, clone);
+    }
+
+    /**
+     * Add a keyframe for the specified Transform at the specified time.
+     *
+     * @param time the animation time when the full Transform should be achieved
+     * (&ge;0, &le;duration)
+     * @param transform the local Transform to apply to the target (not null,
+     * unaffected)
+     */
+    public void addTimeTransform(float time, Transform transform) {
+        if (!(time >= 0f && time <= duration)) {
+            throw new IllegalArgumentException("animation time out of range");
+        }
+
+        Vector3f translation = transform.getTranslation(null);
+        translations.put(time, translation);
+        rotations.put(time, transform.getRotation(null));
+        scales.put(time, transform.getScale(null));
+    }
+
+    /**
+     * Add a keyframe for the specified translation at the specified time.
+     *
+     * @param time the animation time when the full translation should be
+     * achieved (&ge;0, &le;duration)
+     * @param offset the local translation to apply to the target (not null,
+     * unaffected)
+     */
+    public void addTimeTranslation(float time, Vector3f offset) {
+        if (!(time >= 0f && time <= duration)) {
+            throw new IllegalArgumentException("animation time out of range");
+        }
+
+        Vector3f clone = offset.clone();
+        translations.put(time, clone);
+    }
+
+    /**
+     * Create an AnimClip based on the keyframes added to this factory.
+     *
+     * @param target the target for this clip (which is typically a Spatial)
+     * @return a new clip
+     */
+    public AnimClip buildAnimation(HasLocalTransform target) {
+        Set<Float> times = new TreeSet<>();
+        for (int frameI = 0;; ++frameI) {
+            float time = frameI / fps;
+            if (time > duration) {
+                break;
+            }
+            times.add(time);
+        }
+        times.addAll(rotations.keySet());
+        times.addAll(scales.keySet());
+        times.addAll(translations.keySet());
+
+        int numFrames = times.size();
+        float[] timeArray = new float[numFrames];
+        Vector3f[] translateArray = new Vector3f[numFrames];
+        Quaternion[] rotArray = new Quaternion[numFrames];
+        Vector3f[] scaleArray = new Vector3f[numFrames];
+
+        int iFrame = 0;
+        for (float time : times) {
+            timeArray[iFrame] = time;
+            translateArray[iFrame] = interpolateTranslation(time);
+            rotArray[iFrame] = interpolateRotation(time);
+            scaleArray[iFrame] = interpolateScale(time);
+
+            ++iFrame;
+        }
+
+        AnimTrack[] tracks = new AnimTrack[1];
+        tracks[0] = new TransformTrack(target, timeArray, translateArray,
+                rotArray, scaleArray);
+        AnimClip result = new AnimClip(name);
+        result.setTracks(tracks);
+
+        return result;
+    }
+
+    /**
+     * Interpolate successive rotation keyframes for the specified time.
+     *
+     * @param keyFrameTime the animation time (in seconds, &ge;0)
+     * @return a new instance
+     */
+    private Quaternion interpolateRotation(float keyFrameTime) {
+        assert keyFrameTime >= 0f && keyFrameTime <= duration;
+
+        float prev = 0f;
+        float next = duration;
+        for (float key : rotations.keySet()) {
+            if (key <= keyFrameTime && key > prev) {
+                prev = key;
+            }
+            if (key >= keyFrameTime && key < next) {
+                next = key;
+            }
+        }
+        assert prev <= next;
+        Quaternion prevRotation = rotations.get(prev);
+
+        Quaternion result = new Quaternion();
+        if (prev == next || !rotations.containsKey(next)) {
+            result.set(prevRotation);
+
+        } else { // interpolate
+            float fraction = (keyFrameTime - prev) / (next - prev);
+            assert fraction >= 0f && fraction <= 1f;
+            Quaternion nextRotation = rotations.get(next);
+            result.slerp(prevRotation, nextRotation, fraction);
+            /*
+             * XXX slerp() sometimes negates nextRotation,
+             * but usually that's okay because nextRotation and its negative
+             * both represent the same rotation.
+             */
+        }
+
+        return result;
+    }
+
+    /**
+     * Interpolate successive scale keyframes for the specified time.
+     *
+     * @param keyFrameTime the animation time (in seconds, &ge;0)
+     * @return a new instance
+     */
+    private Vector3f interpolateScale(float keyFrameTime) {
+        assert keyFrameTime >= 0f && keyFrameTime <= duration;
+
+        float prev = 0f;
+        float next = duration;
+        for (float key : scales.keySet()) {
+            if (key <= keyFrameTime && key > prev) {
+                prev = key;
+            }
+            if (key >= keyFrameTime && key < next) {
+                next = key;
+            }
+        }
+        assert prev <= next;
+        Vector3f prevScale = scales.get(prev);
+
+        Vector3f result = new Vector3f();
+        if (prev == next || !scales.containsKey(next)) {
+            result.set(prevScale);
+
+        } else { // interpolate
+            float fraction = (keyFrameTime - prev) / (next - prev);
+            assert fraction >= 0f && fraction <= 1f;
+            Vector3f nextScale = scales.get(next);
+            result.interpolateLocal(prevScale, nextScale, fraction);
+        }
+
+        return result;
+    }
+
+    /**
+     * Interpolate successive translation keyframes for the specified time.
+     *
+     * @param keyFrameTime the animation time (in seconds, &ge;0)
+     * @return a new instance
+     */
+    private Vector3f interpolateTranslation(float keyFrameTime) {
+        float prev = 0f;
+        float next = duration;
+        for (float key : translations.keySet()) {
+            if (key <= keyFrameTime && key > prev) {
+                prev = key;
+            }
+            if (key >= keyFrameTime && key < next) {
+                next = key;
+            }
+        }
+        assert prev <= next;
+        Vector3f prevTranslation = translations.get(prev);
+
+        Vector3f result = new Vector3f();
+        if (prev == next || !translations.containsKey(next)) {
+            result.set(prevTranslation);
+
+        } else { // interpolate
+            float fraction = (keyFrameTime - prev) / (next - prev);
+            assert fraction >= 0f && fraction <= 1f;
+            Vector3f nextTranslation = translations.get(next);
+            result.interpolateLocal(prevTranslation, nextTranslation, fraction);
+        }
+
+        return result;
+    }
+}

+ 330 - 0
jme3-core/src/main/java/com/jme3/anim/AnimLayer.java

@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2009-2022 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.anim;
+
+import com.jme3.anim.tween.action.Action;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+
+/**
+ * A named portion of an AnimComposer that can run (at most) one Action at a
+ * time.
+ *
+ * <p>A composer with multiple layers can run multiple actions simultaneously.
+ * For instance, one layer could run a "wave" action on the model's upper body
+ * while another ran a "walk" action on the model's lower body.
+ *
+ * <p>A layer cannot be shared between multiple composers.
+ *
+ * <p>Animation time may advance at a different rate from application time,
+ * based on speedup factors in the composer and the current Action.
+ */
+public class AnimLayer implements JmeCloneable, Savable {
+    /**
+     * The Action currently running on this layer, or null if none.
+     */
+    private Action currentAction;
+    /**
+     * The name of Action currently running on this layer, or null if none.
+     */
+    private String currentActionName;
+    
+    /**
+     * Limits the portion of the model animated by this layer. If null, this
+     * layer can animate the entire model.
+     */
+    private AnimationMask mask;
+    /**
+     * The current animation time, in scaled seconds. Always non-negative.
+     */
+    private double time;
+    /**
+     * The software object (such as an AnimEvent) that currently controls this
+     * layer, or null if unknown.
+     */
+    private Object manager;
+    /**
+     * The name of this layer.
+     */
+    private String name;
+
+    private boolean loop = true;
+    
+    /**
+    * For serialization only. Do not use.
+    */
+    protected AnimLayer() {
+        
+    }
+
+    /**
+     * Instantiates a layer without a manager or a current Action, owned by the
+     * specified composer.
+     *
+     * @param name the layer name (not null)
+     * @param mask the AnimationMask (alias created) or null to allow this layer
+     *     to animate the entire model
+     */
+    AnimLayer(String name, AnimationMask mask) {
+        assert name != null;
+        this.name = name;
+
+        this.mask = mask;
+    }
+
+    /**
+     * Returns the Action that's currently running.
+     *
+     * @return the pre-existing instance, or null if none
+     */
+    public Action getCurrentAction() {
+        return currentAction;
+    }
+
+    /**
+     * Returns the name of the Action that's currently running.
+     *
+     * @return the pre-existing instance, or null if none
+     */
+    public String getCurrentActionName() {
+        return currentActionName;
+    }
+
+    /**
+     * Returns the current manager.
+     *
+     * @return the pre-existing object (such as an AnimEvent) or null for
+     *     unknown
+     */
+    public Object getManager() {
+        return manager;
+    }
+
+    /**
+     * Returns the animation mask.
+     *
+     * @return the pre-existing instance, or null if this layer can animate the
+     *     entire model
+     */
+    public AnimationMask getMask() {
+        return mask;
+    }
+
+    /**
+     * Returns the layer name.
+     *
+     * @return the name of this layer
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns the animation time, in scaled seconds.
+     *
+     * @return the current animation time (not negative)
+     */
+    public double getTime() {
+        return time;
+    }
+
+    /**
+     * Runs the specified Action, starting from time = 0. This cancels any
+     * Action previously running on this layer. By default Action will loop.
+     *
+     * @param actionToRun the Action to run (alias created) or null for no
+     *     action
+     */
+    public void setCurrentAction(Action actionToRun) {
+        this.setCurrentAction(null, actionToRun);
+    }
+
+    /**
+     * Runs the specified Action, starting from time = 0. This cancels any
+     * Action previously running on this layer. By default Action will loop.
+     *
+     * @param actionName the Action name or null for no action name
+     * @param actionToRun the Action to run (alias created) or null for no
+     *     action
+     */
+    public void setCurrentAction(String actionName, Action actionToRun) {
+        this.setCurrentAction(actionName, actionToRun, true);
+    }
+
+    /**
+     * Runs the specified Action, starting from time = 0. This cancels any
+     * Action previously running on this layer.
+     *
+     * @param actionName the Action name or null for no action name
+     * @param actionToRun the Action to run (alias created) or null for no
+     *     action
+     * @param loop true if Action must loop. If it is false, Action will be
+     *     removed after finished running
+     */
+    public void setCurrentAction(String actionName, Action actionToRun, boolean loop) {
+        this.time = 0.0;
+        this.currentAction = actionToRun;
+        this.currentActionName = actionName;
+        this.loop = loop;
+    }
+
+    /**
+     * Assigns the specified manager. This cancels any manager previously
+     * assigned.
+     *
+     * @param manager the desired manager (such as an AnimEvent, alias created)
+     *     or null for unknown manager
+     */
+    public void setManager(Object manager) {
+        this.manager = manager;
+    }
+
+    /**
+     * Changes the animation time, wrapping the specified time to fit the
+     * current Action. An Action must be running.
+     *
+     * @param animationTime the desired time (in scaled seconds)
+     */
+    public void setTime(double animationTime) {
+        double length = currentAction.getLength();
+        if (animationTime >= 0.0) {
+            time = animationTime % length;
+        } else {
+            time = (animationTime % length) + length;
+        }
+    }
+
+    /**
+     * @return True if the Action will keep looping after it is done playing,
+     * otherwise, returns false
+     */
+    public boolean isLooping() {
+        return loop;
+    }
+
+    /**
+     * Sets the looping mode for this layer. The default is true.
+     *
+     * @param loop True if the action should keep looping after it is done
+     * playing
+     */
+    public void setLooping(boolean loop) {
+        this.loop = loop;
+    }
+
+    /**
+     * Updates the animation time and the current Action during a
+     * controlUpdate().
+     *
+     * @param appDeltaTimeInSeconds the amount application time to advance the
+     *     current Action, in seconds
+     * @param globalSpeed the global speed applied to all layers.
+     */
+    void update(float appDeltaTimeInSeconds, float globalSpeed) {
+        Action action = currentAction;
+        if (action == null) {
+            return;
+        }
+
+        double speedup = action.getSpeed() * globalSpeed;
+        double scaledDeltaTime = speedup * appDeltaTimeInSeconds;
+        time += scaledDeltaTime;
+
+        // wrap negative times to the [0, length] range:
+        if (time < 0.0) {
+            double length = action.getLength();
+            time = (time % length + length) % length;
+        }
+
+        // update the current Action, filtered by this layer's mask:
+        action.setMask(mask);
+        boolean running = action.interpolate(time);
+        action.setMask(null);
+
+        if (!running) { // went past the end of the current Action
+            time = 0.0;
+            if (!loop) {
+                // Clear the current action
+                setCurrentAction(null);
+            }
+        }
+    }
+
+    /**
+     * Converts this shallow-cloned layer into a deep-cloned one, using the
+     * specified Cloner and original to resolve copied fields.
+     *
+     * <p>The clone's current Action gets nulled out. Its manager and mask get
+     * aliased to the original's manager and mask.
+     *
+     * @param cloner the Cloner that's cloning this layer (not null)
+     * @param original the instance from which this layer was shallow-cloned
+     *     (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        currentAction = null;
+        currentActionName = null;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            AnimLayer clone = (AnimLayer) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(name, "name", null);
+        if (mask instanceof Savable) {
+            oc.write((Savable) mask, "mask", null);
+        }
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        name = ic.readString("name", null);
+        mask = (AnimationMask) ic.readSavable("mask", null);
+    }
+}

+ 17 - 2
jme3-core/src/main/java/com/jme3/anim/AnimTrack.java

@@ -3,10 +3,25 @@ package com.jme3.anim;
 import com.jme3.export.Savable;
 import com.jme3.util.clone.JmeCloneable;
 
+/**
+ * Interface to derive animation data from a track.
+ *
+ * @param <T> the type of data that's being animated, such as Transform
+ */
 public interface AnimTrack<T> extends Savable, JmeCloneable {
 
+    /**
+     * Determine the track value for the specified time.
+     *
+     * @param time the track time (in seconds)
+     * @param store storage for the value (not null, modified)
+     */
     public void getDataAtTime(double time, T store);
-    public double getLength();
-
 
+    /**
+     * Determine the duration of the track.
+     *
+     * @return the duration (in seconds, &ge;0)
+     */
+    public double getLength();
 }

+ 7 - 1
jme3-core/src/main/java/com/jme3/anim/AnimationMask.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,6 +38,12 @@ package com.jme3.anim;
  */
 public interface AnimationMask {
 
+    /**
+     * Test whether the animation should be applied to the specified element.
+     *
+     * @param target the target element
+     * @return true if animation should be applied, otherwise false
+     */
     boolean contains(Object target);
 
 }

+ 36 - 16
jme3-core/src/main/java/com/jme3/anim/Armature.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -37,8 +37,8 @@ import com.jme3.export.*;
 import com.jme3.math.Matrix4f;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
-
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 import java.util.*;
 
 /**
@@ -78,7 +78,7 @@ public class Armature implements JmeCloneable, Savable {
         for (int i = jointList.length - 1; i >= 0; i--) {
             Joint joint = jointList[i];
             joint.setId(i);
-            instanciateJointModelTransform(joint);
+            instantiateJointModelTransform(joint);
             if (joint.getParent() == null) {
                 rootJointList.add(joint);
             }
@@ -113,7 +113,7 @@ public class Armature implements JmeCloneable, Savable {
      * Sets the JointModelTransform implementation
      * Default is {@link MatrixJointModelTransform}
      *
-     * @param modelTransformClass
+     * @param modelTransformClass which implementation to use
      * @see JointModelTransform
      * @see MatrixJointModelTransform
      * @see SeparateJointModelTransform
@@ -124,14 +124,16 @@ public class Armature implements JmeCloneable, Savable {
             return;
         }
         for (Joint joint : jointList) {
-            instanciateJointModelTransform(joint);
+            instantiateJointModelTransform(joint);
         }
     }
 
-    private void instanciateJointModelTransform(Joint joint) {
+    private void instantiateJointModelTransform(Joint joint) {
         try {
-            joint.setJointModelTransform(modelTransformClass.newInstance());
-        } catch (InstantiationException | IllegalAccessException e) {
+            joint.setJointModelTransform(modelTransformClass.getDeclaredConstructor().newInstance());
+        } catch (InstantiationException | IllegalAccessException
+                | IllegalArgumentException | InvocationTargetException
+                | NoSuchMethodException | SecurityException e) {
             throw new IllegalArgumentException(e);
         }
     }
@@ -145,6 +147,11 @@ public class Armature implements JmeCloneable, Savable {
         return rootJoints;
     }
 
+    /**
+     * Access all joints in this Armature.
+     *
+     * @return a new list of pre-existing joints
+     */
     public List<Joint> getJointList() {
         return Arrays.asList(jointList);
     }
@@ -152,7 +159,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * return a joint for the given index
      *
-     * @param index
+     * @param index a zero-based joint index (&ge;0)
      * @return the pre-existing instance
      */
     public Joint getJoint(int index) {
@@ -162,7 +169,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * returns the joint with the given name
      *
-     * @param name
+     * @param name the name to search for
      * @return the pre-existing instance or null if not found
      */
     public Joint getJoint(String name) {
@@ -177,7 +184,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * returns the bone index of the given bone
      *
-     * @param joint
+     * @param joint the Joint to search for
      * @return the index (&ge;0) or -1 if not found
      */
     public int getJointIndex(Joint joint) {
@@ -193,7 +200,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * returns the joint index of the joint that has the given name
      *
-     * @param name
+     * @param name the name to search for
      * @return the index (&ge;0) or -1 if not found
      */
     public int getJointIndex(String name) {
@@ -221,7 +228,7 @@ public class Armature implements JmeCloneable, Savable {
     }
 
     /**
-     * This methods sets this armature in its bind pose (aligned with the mesh to deform)
+     * This method sets this armature to its bind pose (aligned with the mesh to deform).
      * Note that this is only useful for debugging purpose.
      */
     public void applyBindPose() {
@@ -286,11 +293,17 @@ public class Armature implements JmeCloneable, Savable {
         this.jointList = cloner.clone(jointList);
         this.skinningMatrixes = cloner.clone(skinningMatrixes);
         for (Joint joint : jointList) {
-            instanciateJointModelTransform(joint);
+            instantiateJointModelTransform(joint);
         }
     }
 
-
+    /**
+     * De-serialize this Armature from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param im the importer to read from (not null)
+     * @throws IOException from the importer
+     */
     @Override
     @SuppressWarnings("unchecked")
     public void read(JmeImporter im) throws IOException {
@@ -314,7 +327,7 @@ public class Armature implements JmeCloneable, Savable {
         int i = 0;
         for (Joint joint : jointList) {
             joint.setId(i++);
-            instanciateJointModelTransform(joint);
+            instantiateJointModelTransform(joint);
         }
         createSkinningMatrices();
 
@@ -324,6 +337,13 @@ public class Armature implements JmeCloneable, Savable {
         applyInitialPose();
     }
 
+    /**
+     * Serialize this Armature to the specified exporter, for example when
+     * saving to a J3O file.
+     *
+     * @param ex the exporter to write to (not null)
+     * @throws IOException from the exporter
+     */
     @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule output = ex.getCapsule(this);

+ 169 - 4
jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java

@@ -1,33 +1,151 @@
+/*
+ * Copyright (c) 2009-2023 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.anim;
 
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import java.io.IOException;
 import java.util.BitSet;
 
-public class ArmatureMask implements AnimationMask {
+/**
+ * An AnimationMask to select joints from a single Armature.
+ */
+public class ArmatureMask implements AnimationMask, Savable {
 
     private BitSet affectedJoints = new BitSet();
 
+    /**
+     * Instantiate a mask that affects no joints.
+     */
+    public ArmatureMask() {
+        // do nothing
+    }
+
+    /**
+     * Instantiate a mask that affects all joints in the specified Armature.
+     *
+     * @param armature the Armature containing the joints (not null, unaffected)
+     */
+    public ArmatureMask(Armature armature) {
+        int numJoints = armature.getJointCount();
+        affectedJoints.set(0, numJoints);
+    }
+
+    /**
+     * Remove all joints affected by the specified ArmatureMask.
+     *
+     * @param removeMask the set of joints to remove (not null, unaffected)
+     * @return this
+     */
+    public ArmatureMask remove(ArmatureMask removeMask) {
+        BitSet removeBits = removeMask.getAffectedJoints();
+        affectedJoints.andNot(removeBits);
+
+        return this;
+    }
+
+    private BitSet getAffectedJoints() {
+        return affectedJoints;
+    }
+
+    /**
+     * Remove the named joints.
+     *
+     * @param armature the Armature containing the joints (not null, unaffected)
+     * @param jointNames the names of the joints to be removed
+     * @return this
+     *
+     * @throws IllegalArgumentException if it can not find the joint
+     *          with the specified name on the provided armature
+     */
+    public ArmatureMask removeJoints(Armature armature, String... jointNames) {
+        for (String jointName : jointNames) {
+            Joint joint = findJoint(armature, jointName);
+            int jointId = joint.getId();
+            affectedJoints.clear(jointId);
+        }
+
+        return this;
+    }
+
     @Override
     public boolean contains(Object target) {
         return affectedJoints.get(((Joint) target).getId());
     }
 
+    /**
+     * Create an ArmatureMask that selects the named Joint and all its
+     * descendants.
+     *
+     * @param armature the Armature containing the joints (not null)
+     * @param fromJoint the name of the ancestor joint
+     * @return a new mask
+     *
+     * @throws IllegalArgumentException if it can not find the joint
+     *          with the specified name on the provided armature
+     */
     public static ArmatureMask createMask(Armature armature, String fromJoint) {
         ArmatureMask mask = new ArmatureMask();
         mask.addFromJoint(armature, fromJoint);
         return mask;
     }
 
+    /**
+     * Create an ArmatureMask that selects the named joints.
+     *
+     * @param armature the Armature containing the joints (not null)
+     * @param joints the names of the joints to be included
+     * @return a new mask
+     *
+     * @throws IllegalArgumentException if it can not find the joint
+     *          with the specified name on the provided armature
+     */
     public static ArmatureMask createMask(Armature armature, String... joints) {
         ArmatureMask mask = new ArmatureMask();
         mask.addBones(armature, joints);
-        for (String joint : joints) {
-            mask.affectedJoints.set(armature.getJoint(joint).getId());
-        }
         return mask;
     }
 
     /**
      * Add joints to be influenced by this animation mask.
+     * 
+     * @param armature the Armature containing the joints
+     * @param jointNames the names of the joints to be influenced
+     *
+     * @throws IllegalArgumentException if it can not find the joint
+     *          with the specified name on the provided armature
      */
     public void addBones(Armature armature, String... jointNames) {
         for (String jointName : jointNames) {
@@ -46,6 +164,12 @@ public class ArmatureMask implements AnimationMask {
 
     /**
      * Add a joint and all its sub armature joints to be influenced by this animation mask.
+     * 
+     * @param armature the Armature containing the ancestor joint
+     * @param jointName the names of the ancestor joint
+     *
+     * @throws IllegalArgumentException if it can not find the joint
+     *          with the specified name on the provided armature
      */
     public void addFromJoint(Armature armature, String jointName) {
         Joint joint = findJoint(armature, jointName);
@@ -59,4 +183,45 @@ public class ArmatureMask implements AnimationMask {
         }
     }
 
+    /**
+     * Add the specified Joint and all its ancestors.
+     *
+     * @param start the starting point (may be null, unaffected)
+     * @return this
+     */
+    public ArmatureMask addAncestors(Joint start) {
+        for (Joint cur = start; cur != null; cur = cur.getParent()) {
+            int jointId = cur.getId();
+            affectedJoints.set(jointId);
+        }
+
+        return this;
+    }
+
+    /**
+     * Remove the specified Joint and all its ancestors.
+     *
+     * @param start the starting point (may be null, unaffected)
+     * @return this
+     */
+    public ArmatureMask removeAncestors(Joint start) {
+        for (Joint cur = start; cur != null; cur = cur.getParent()) {
+            int jointId = cur.getId();
+            affectedJoints.clear(jointId);
+        }
+
+        return this;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(affectedJoints, "affectedJoints", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        affectedJoints = ic.readBitSet("affectedJoints", null);
+    }
 }

+ 148 - 8
jme3-core/src/main/java/com/jme3/anim/Joint.java

@@ -88,16 +88,23 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
      */
     private Matrix4f inverseModelBindMatrix = new Matrix4f();
 
-
+    /**
+     * Instantiate a nameless Joint.
+     */
     public Joint() {
     }
 
+    /**
+     * Instantiate a Joint with the specified name.
+     *
+     * @param name the desired name
+     */
     public Joint(String name) {
         this.name = name;
     }
 
     /**
-     * Updates world transforms for this bone and it's children.
+     * Updates world transforms for this bone and its children.
      */
     public final void update() {
         this.updateModelTransforms();
@@ -108,11 +115,11 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
     }
 
     /**
-     * Updates the model transforms for this bone, and, possibly the attach node
+     * Updates the model transforms for this bone and for the attachments node
      * if not null.
      * <p>
      * The model transform of this bone is computed by combining the parent's
-     * model transform with this bones' local transform.
+     * model transform with this bone's local transform.
      */
     public final void updateModelTransforms() {
         jointModelTransform.updateModelTransform(localTransform, parent);
@@ -160,9 +167,9 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
      * have already been computed, otherwise this method will return undefined
      * results.
      *
-     * @param outTransform
+     * @param outTransform storage for the result (modified)
      */
-    void getOffsetTransform(Matrix4f outTransform) {
+    protected void getOffsetTransform(Matrix4f outTransform) {
         jointModelTransform.getOffsetTransform(outTransform, inverseModelBindMatrix);
     }
 
@@ -206,64 +213,139 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
         }
     }
 
+    /**
+     * Access the accumulated model transform.
+     *
+     * @return the pre-existing instance
+     */
     protected JointModelTransform getJointModelTransform() {
         return jointModelTransform;
     }
 
+    /**
+     * Replace the accumulated model transform.
+     *
+     * @param jointModelTransform the transform to use (alias created)
+     */
     protected void setJointModelTransform(JointModelTransform jointModelTransform) {
         this.jointModelTransform = jointModelTransform;
     }
 
+    /**
+     * Access the local translation vector.
+     *
+     * @return the pre-existing vector
+     */
     public Vector3f getLocalTranslation() {
         return localTransform.getTranslation();
     }
 
+    /**
+     * Access the local rotation.
+     *
+     * @return the pre-existing Quaternion
+     */
     public Quaternion getLocalRotation() {
         return localTransform.getRotation();
     }
 
+    /**
+     * Access the local scale vector.
+     *
+     * @return the pre-existing vector
+     */
     public Vector3f getLocalScale() {
         return localTransform.getScale();
     }
 
+    /**
+     * Alter the local translation vector.
+     *
+     * @param translation the desired offset vector (not null, unaffected)
+     */
     public void setLocalTranslation(Vector3f translation) {
         localTransform.setTranslation(translation);
     }
 
+    /**
+     * Alter the local rotation.
+     *
+     * @param rotation the desired rotation (not null, unaffected)
+     */
     public void setLocalRotation(Quaternion rotation) {
         localTransform.setRotation(rotation);
     }
 
+    /**
+     * Alter the local scale vector.
+     *
+     * @param scale the desired scale factors (not null, unaffected)
+     */
     public void setLocalScale(Vector3f scale) {
         localTransform.setScale(scale);
     }
 
+    /**
+     * Add the specified Joint as a child.
+     *
+     * @param child the Joint to add (not null, modified)
+     */
     public void addChild(Joint child) {
         children.add(child);
         child.parent = this;
     }
 
+    /**
+     * Alter the name.
+     *
+     * @param name the desired name
+     */
     public void setName(String name) {
         this.name = name;
     }
 
+    /**
+     * Alter the local transform.
+     *
+     * @param localTransform the desired Transform (not null, unaffected)
+     */
     @Override
     public void setLocalTransform(Transform localTransform) {
         this.localTransform.set(localTransform);
     }
 
+    /**
+     * Replace the inverse model bind matrix.
+     *
+     * @param inverseModelBindMatrix the matrix to use (alias created)
+     */
     public void setInverseModelBindMatrix(Matrix4f inverseModelBindMatrix) {
         this.inverseModelBindMatrix = inverseModelBindMatrix;
     }
 
+    /**
+     * Determine the name.
+     *
+     * @return the name
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * Access the parent joint.
+     *
+     * @return the pre-existing instance, or null if this is a root joint
+     */
     public Joint getParent() {
         return parent;
     }
 
+    /**
+     * Access the list of child joints.
+     *
+     * @return the pre-existing list
+     */
     public List<Joint> getChildren() {
         return children;
     }
@@ -276,8 +358,9 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
      * @param jointIndex this bone's index in its armature (&ge;0)
      * @param targets    a list of geometries animated by this bone's skeleton (not
      *                   null, unaffected)
+     * @return the attachments node (not null)
      */
-    Node getAttachmentsNode(int jointIndex, SafeArrayList<Geometry> targets) {
+    protected Node getAttachmentsNode(int jointIndex, SafeArrayList<Geometry> targets) {
         targetGeometry = null;
         /*
          * Search for a geometry animated by this particular bone.
@@ -300,31 +383,66 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
         return attachedNode;
     }
 
+    /**
+     * Access the initial transform.
+     *
+     * @return the pre-existing instance
+     */
     public Transform getInitialTransform() {
         return initialTransform;
     }
 
+    /**
+     * Access the local transform.
+     *
+     * @return the pre-existing instance
+     */
     @Override
     public Transform getLocalTransform() {
         return localTransform;
     }
 
+    /**
+     * Determine the model transform.
+     *
+     * @return a shared instance
+     */
     public Transform getModelTransform() {
         return jointModelTransform.getModelTransform();
     }
 
+    /**
+     * Determine the inverse model bind matrix.
+     *
+     * @return the pre-existing instance
+     */
     public Matrix4f getInverseModelBindMatrix() {
         return inverseModelBindMatrix;
     }
 
+    /**
+     * Determine this joint's index in the Armature that contains it.
+     *
+     * @return an index (&ge;0)
+     */
     public int getId() {
         return id;
     }
 
+    /**
+     * Alter this joint's index in the Armature that contains it.
+     *
+     * @param id the desired index (&ge;0)
+     */
     public void setId(int id) {
         this.id = id;
     }
 
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
     @Override
     public Object jmeClone() {
         try {
@@ -335,6 +453,15 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
         }
     }
 
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned Joint into a deep-cloned one, using the specified Cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the Cloner that's cloning this Joint (not null)
+     * @param original the instance from which this Joint was shallow-cloned
+     * (not null, unaffected)
+     */
     @Override
     public void cloneFields(Cloner cloner, Object original) {
         this.children = cloner.clone(children);
@@ -346,7 +473,13 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
         this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix);
     }
 
-
+    /**
+     * De-serialize this Joint from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param im the importer to use (not null)
+     * @throws IOException from the importer
+     */
     @Override
     @SuppressWarnings("unchecked")
     public void read(JmeImporter im) throws IOException {
@@ -364,6 +497,13 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
         }
     }
 
+    /**
+     * Serialize this Joint to the specified exporter, for example when saving
+     * to a J3O file.
+     *
+     * @param ex the exporter to write to (not null)
+     * @throws IOException from the exporter
+     */
     @Override
     @SuppressWarnings("unchecked")
     public void write(JmeExporter ex) throws IOException {

+ 7 - 2
jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java

@@ -10,8 +10,8 @@ import com.jme3.math.Transform;
  */
 public class MatrixJointModelTransform implements JointModelTransform {
 
-    private Matrix4f modelTransformMatrix = new Matrix4f();
-    private Transform modelTransform = new Transform();
+    final private Matrix4f modelTransformMatrix = new Matrix4f();
+    final private Transform modelTransform = new Transform();
 
     @Override
     public void updateModelTransform(Transform localTransform, Joint parent) {
@@ -36,6 +36,11 @@ public class MatrixJointModelTransform implements JointModelTransform {
         localTransform.fromTransformMatrix(modelTransformMatrix);
     }
 
+    /**
+     * Access the model transform.
+     *
+     * @return the pre-existing instance 
+     */
     public Matrix4f getModelTransformMatrix() {
         return modelTransformMatrix;
     }

+ 123 - 20
jme3-core/src/main/java/com/jme3/anim/MorphControl.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,8 +31,10 @@
  */
 package com.jme3.anim;
 
-import com.jme3.export.Savable;
-import com.jme3.material.*;
+import com.jme3.export.*;
+import com.jme3.material.MatParam;
+import com.jme3.material.MatParamOverride;
+import com.jme3.material.Material;
 import com.jme3.renderer.*;
 import com.jme3.scene.*;
 import com.jme3.scene.control.AbstractControl;
@@ -40,8 +42,10 @@ import com.jme3.scene.mesh.MorphTarget;
 import com.jme3.shader.VarType;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.SafeArrayList;
-
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
 import java.nio.FloatBuffer;
+import java.util.ArrayList;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -50,6 +54,9 @@ import java.util.logging.Logger;
  * All stock shaders only support morphing these 3 buffers, but note that MorphTargets can have any type of buffers.
  * If you want to use other types of buffers you will need a custom MorphControl and a custom shader.
  *
+ * Note that if morphed children are attached to or detached from the sub graph after the MorphControl is added to
+ * spatial, you must detach and attach the control again for the changes to get reflected.
+ *
  * @author Rémy Bouquet
  */
 public class MorphControl extends AbstractControl implements Savable {
@@ -59,6 +66,9 @@ public class MorphControl extends AbstractControl implements Savable {
     private static final int MAX_MORPH_BUFFERS = 14;
     private final static float MIN_WEIGHT = 0.005f;
 
+    private static final String TAG_APPROXIMATE = "approximateTangents";
+    private static final String TAG_TARGETS = "targets";
+
     private SafeArrayList<Geometry> targets = new SafeArrayList<>(Geometry.class);
     private TargetLocator targetLocator = new TargetLocator();
 
@@ -72,22 +82,31 @@ public class MorphControl extends AbstractControl implements Savable {
     private static final VertexBuffer.Type bufferTypes[] = VertexBuffer.Type.values();
 
     @Override
-    protected void controlUpdate(float tpf) {
-        if (!enabled) {
-            return;
+    public void setSpatial(Spatial spatial) {
+        super.setSpatial(spatial);
+
+        // Remove matparam override from the old targets
+        for (Geometry target : targets.getArray()) {
+            target.removeMatParamOverride(nullNumberOfBones);
         }
+
         // gathering geometries in the sub graph.
-        // This must be done in the update phase as the gathering might add a matparam override
+        // This must not be done in the render phase as the gathering might add a matparam override
+        // which then will throw an IllegalStateException if done in the render phase.
         targets.clear();
-        this.spatial.depthFirstTraversal(targetLocator);
+        if (spatial != null) {
+            spatial.depthFirstTraversal(targetLocator);
+        }
+    }
+
+    @Override
+    protected void controlUpdate(float tpf) {
+
     }
 
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
-        if (!enabled) {
-            return;
-        }
-        for (Geometry geom : targets) {
+        for (Geometry geom : targets.getArray()) {
             Mesh mesh = geom.getMesh();
             if (!geom.isDirtyMorph()) {
                 continue;
@@ -123,7 +142,7 @@ public class MorphControl extends AbstractControl implements Savable {
                 lastGpuTargetIndex = i;
                 // binding the morph target's buffers to the mesh morph buffers.
                 MorphTarget t = morphTargets[i];
-                boundBufferIdx = bindMorphtargetBuffer(mesh, targetNumBuffers, boundBufferIdx, t);
+                boundBufferIdx = bindMorphTargetBuffer(mesh, targetNumBuffers, boundBufferIdx, t);
                 // setting the weight in the mat param array
                 matWeights[nbGPUTargets] = weights[i];
                 nbGPUTargets++;
@@ -160,7 +179,7 @@ public class MorphControl extends AbstractControl implements Savable {
                 writeCpuBuffer(targetNumBuffers, mt);
 
                 // binding the merged morph target
-                bindMorphtargetBuffer(mesh, targetNumBuffers, (nbGPUTargets - 1) * targetNumBuffers, mt);
+                bindMorphTargetBuffer(mesh, targetNumBuffers, (nbGPUTargets - 1) * targetNumBuffers, mt);
 
                 // setting the eight of the merged targets
                 matWeights[nbGPUTargets - 1] = cpuWeightSum;
@@ -184,7 +203,7 @@ public class MorphControl extends AbstractControl implements Savable {
 
         MatParam param = mat.getParam("MorphWeights");
         if (param == null) {
-            // init the mat param if it doesn't exists.
+            // init the mat param if it doesn't exist.
             float[] wts = new float[maxGPUTargets];
             mat.setParam("MorphWeights", VarType.FloatArray, wts);
         }
@@ -202,18 +221,22 @@ public class MorphControl extends AbstractControl implements Savable {
                 rm.preloadScene(spatial);
                 compilationOk = true;
             } catch (RendererException e) {
-                logger.log(Level.FINE, geom.getName() + ": failed at " + maxGPUTargets);
-                // the compilation failed let's decrement the number of targets an try again.
+                if (logger.isLoggable(Level.FINE)) {
+                    logger.log(Level.FINE, "{0}: failed at {1}", new Object[]{geom.getName(), maxGPUTargets});
+                }
+                // the compilation failed let's decrement the number of targets and try again.
                 maxGPUTargets--;
             }
         }
-        logger.log(Level.FINE, geom.getName() + ": " + maxGPUTargets);
+        if (logger.isLoggable(Level.FINE)) {
+            logger.log(Level.FINE, "{0}: {1}", new Object[]{geom.getName(), maxGPUTargets});
+        }
         // set the number of GPU morph on the geom to not have to recompute it next frame.
         geom.setNbSimultaneousGPUMorph(maxGPUTargets);
         return maxGPUTargets;
     }
 
-    private int bindMorphtargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) {
+    private int bindMorphTargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) {
         int start = VertexBuffer.Type.MorphTarget0.ordinal();
         if (targetNumBuffers >= 1) {
             activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position));
@@ -353,14 +376,94 @@ public class MorphControl extends AbstractControl implements Savable {
         return renderer.getLimits().get(Limits.VertexAttributes) - nbUsedBuffers;
     }
 
+    /**
+     * Alter whether this Control approximates tangents.
+     *
+     * @param approximateTangents true to approximate tangents, false to get
+     * them from a VertexBuffer
+     */
     public void setApproximateTangents(boolean approximateTangents) {
         this.approximateTangents = approximateTangents;
     }
 
+    /**
+     * Test whether this Control approximates tangents.
+     *
+     * @return true if approximating tangents, false if getting them from a
+     * VertexBuffer
+     */
     public boolean isApproximateTangents() {
         return approximateTangents;
     }
 
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned Control into a deep-cloned one, using the specified Cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the Cloner that's cloning this Control (not null, modified)
+     * @param original the instance from which this Control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        targets = cloner.clone(targets);
+        targetLocator = new TargetLocator();
+        nullNumberOfBones = cloner.clone(nullNumberOfBones);
+        tmpPosArray = null;
+        tmpNormArray = null;
+        tmpTanArray = null;
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public MorphControl jmeClone() {
+        try {
+            MorphControl clone = (MorphControl) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * De-serialize this Control from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param importer (not null)
+     * @throws IOException from the importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter importer) throws IOException {
+        super.read(importer);
+        InputCapsule capsule = importer.getCapsule(this);
+        approximateTangents = capsule.readBoolean(TAG_APPROXIMATE, true);
+        targets.addAll(capsule.readSavableArrayList(TAG_TARGETS, null));
+    }
+
+    /**
+     * Serialize this Control to the specified exporter, for example when saving
+     * to a J3O file.
+     *
+     * @param exporter (not null)
+     * @throws IOException from the exporter
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void write(JmeExporter exporter) throws IOException {
+        super.write(exporter);
+        OutputCapsule capsule = exporter.getCapsule(this);
+        capsule.write(approximateTangents, TAG_APPROXIMATE, true);
+        capsule.writeSavableArrayList(new ArrayList(targets), TAG_TARGETS, null);
+    }
+
     private class TargetLocator extends SceneGraphVisitorAdapter {
         @Override
         public void visit(Geometry geom) {

+ 106 - 12
jme3-core/src/main/java/com/jme3/anim/MorphTrack.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -52,7 +52,11 @@ public class MorphTrack implements AnimTrack<float[]> {
      * Weights and times for track.
      */
     private float[] weights;
-    private FrameInterpolator interpolator = FrameInterpolator.DEFAULT;
+    /**
+     * The interpolator to use, or null to always use the default interpolator
+     * of the current thread.
+     */
+    private FrameInterpolator interpolator = null;
     private float[] times;
     private int nbMorphTargets;
 
@@ -65,10 +69,13 @@ public class MorphTrack implements AnimTrack<float[]> {
     /**
      * Creates a morph track with the given Geometry as a target
      *
+     * @param target   the desired target (alias created)
      * @param times    a float array with the time of each frame (alias created
      *                 -- do not modify after passing it to this constructor)
      * @param weights  the morphs for each frames (alias created -- do not
      *                 modify after passing it to this constructor)
+     * @param nbMorphTargets
+     *                 the desired number of morph targets
      */
     public MorphTrack(Geometry target, float[] times, float[] weights, int nbMorphTargets) {
         this.target = target;
@@ -85,6 +92,31 @@ public class MorphTrack implements AnimTrack<float[]> {
         return weights;
     }
 
+    /**
+     * Set the weights for this morph track. Note that the number of weights
+     * must equal the number of frames times the number of morph targets.
+     *
+     * @param weights  the weights of the morphs for each frame (alias created
+     *                 -- do not modify after passing it to this setter)
+     *
+     * @throws IllegalStateException if this track does not have times set
+     * @throws IllegalArgumentException if weights is an empty array or if
+     *         the number of weights violates the frame count constraint
+     */
+    public void setKeyframesWeight(float[] weights) {
+        if (times == null) {
+            throw new IllegalStateException("MorphTrack doesn't have any time for key frames, please call setTimes first");
+        }
+        if (weights.length == 0) {
+            throw new IllegalArgumentException("MorphTrack with no weight keyframes!");
+        }
+        if (times.length * nbMorphTargets != weights.length) {
+            throw new IllegalArgumentException("weights.length must equal nbMorphTargets * times.length");
+        }
+
+        this.weights = weights;
+    }
+
     /**
      * returns the arrays of time for this track
      *
@@ -99,10 +131,11 @@ public class MorphTrack implements AnimTrack<float[]> {
      *
      * @param times  the keyframes times (alias created -- do not modify after
      *               passing it to this setter)
+     * @throws IllegalArgumentException if times is empty
      */
     public void setTimes(float[] times) {
         if (times.length == 0) {
-            throw new RuntimeException("TransformTrack with no keyframes!");
+            throw new IllegalArgumentException("TransformTrack with no keyframes!");
         }
         this.times = times;
         length = times[times.length - 1] - times[0];
@@ -110,7 +143,8 @@ public class MorphTrack implements AnimTrack<float[]> {
 
 
     /**
-     * Set the weight for this morph track
+     * Sets the times and weights for this morph track. Note that the number of weights
+     * must equal the number of frames times the number of morph targets.
      *
      * @param times    a float array with the time of each frame (alias created
      *                 -- do not modify after passing it to this setter)
@@ -118,16 +152,37 @@ public class MorphTrack implements AnimTrack<float[]> {
      *                 -- do not modify after passing it to this setter)
      */
     public void setKeyframes(float[] times, float[] weights) {
-        setTimes(times);
+        if (times != null) {
+            setTimes(times);
+        }
         if (weights != null) {
-            if (times == null) {
-                throw new RuntimeException("MorphTrack doesn't have any time for key frames, please call setTimes first");
-            }
+            setKeyframesWeight(weights);
+        }
+    }
 
-            this.weights = weights;
+    /**
+     * @return the number of morph targets
+     */
+    public int getNbMorphTargets() {
+        return nbMorphTargets;
+    }
 
-            assert times.length == weights.length;
+    /**
+     * Sets the number of morph targets and the corresponding weights.
+     * Note that the number of weights must equal the number of frames times the number of morph targets.
+     *
+     * @param weights        the weights for each frame (alias created
+     *                       -- do not modify after passing it to this setter)
+     * @param nbMorphTargets the desired number of morph targets
+     * @throws IllegalArgumentException if the number of weights and the new
+     *         number of morph targets violate the frame count constraint
+     */
+    public void setNbMorphTargets(float[] weights, int nbMorphTargets) {
+        if (times.length * nbMorphTargets != weights.length) {
+            throw new IllegalArgumentException("weights.length must equal nbMorphTargets * times.length");
         }
+        this.nbMorphTargets = nbMorphTargets;
+        setKeyframesWeight(weights);
     }
 
     @Override
@@ -168,21 +223,54 @@ public class MorphTrack implements AnimTrack<float[]> {
                     / (times[endFrame] - times[startFrame]);
         }
 
-        interpolator.interpolateWeights(blend, startFrame, weights, nbMorphTargets, store);
+        FrameInterpolator fi = (interpolator == null)
+                ? FrameInterpolator.getThreadDefault() : interpolator;
+        fi.interpolateWeights(blend, startFrame, weights, nbMorphTargets, store);
+    }
+
+    /**
+     * Access the FrameInterpolator.
+     *
+     * @return the pre-existing instance or null
+     */
+    public FrameInterpolator getFrameInterpolator() {
+        return interpolator;
     }
 
+    /**
+     * Replace the FrameInterpolator.
+     *
+     * @param interpolator the interpolator to use (alias created)
+     */
     public void setFrameInterpolator(FrameInterpolator interpolator) {
         this.interpolator = interpolator;
     }
 
+    /**
+     * Access the target geometry.
+     *
+     * @return the pre-existing instance
+     */
     public Geometry getTarget() {
         return target;
     }
 
+    /**
+     * Replace the target geometry.
+     *
+     * @param target the Geometry to use as the target (alias created)
+     */
     public void setTarget(Geometry target) {
         this.target = target;
     }
 
+    /**
+     * Serialize this track to the specified exporter, for example when saving
+     * to a J3O file.
+     *
+     * @param ex the exporter to write to (not null)
+     * @throws IOException from the exporter
+     */
     @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
@@ -192,6 +280,13 @@ public class MorphTrack implements AnimTrack<float[]> {
         oc.write(nbMorphTargets, "nbMorphTargets", 0);
     }
 
+    /**
+     * De-serialize this track from the specified importer, for example when
+     * loading from a J3O file.
+     *
+     * @param im the importer to use (not null)
+     * @throws IOException from the importer
+     */
     @Override
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
@@ -218,5 +313,4 @@ public class MorphTrack implements AnimTrack<float[]> {
         // Note: interpolator, times, and weights are not cloned
     }
 
-
 }

Some files were not shown because too many files changed in this diff