Browse Source

Merge branch 'develop' into feature/nullkiller2

Mircea TheHonestCTO 3 months ago
parent
commit
5a8ffdeb8b
100 changed files with 1325 additions and 7403 deletions
  1. 89 97
      .github/workflows/github.yml
  2. 44 0
      .github/workflows/qt_translation_update.yml
  3. 0 2
      .gitignore
  4. 3 0
      .gitmodules
  5. 0 4
      AI/CMakeLists.txt
  6. 7 0
      CI/android/gradle_daily_props.py
  7. 0 4
      CI/before_install/android.sh
  8. 1 1
      CI/before_install/linux_qt5.sh
  9. 1 1
      CI/before_install/linux_qt6.sh
  10. 1 1
      CI/before_install/macos.sh
  11. 0 35
      CI/before_install/mingw.sh
  12. 0 17
      CI/before_install/msvc.sh
  13. 0 5
      CI/conan/android-32
  14. 0 4
      CI/conan/android-32-ndk
  15. 0 5
      CI/conan/android-64
  16. 0 4
      CI/conan/android-64-ndk
  17. 0 10
      CI/conan/base/android
  18. 0 12
      CI/conan/base/apple
  19. 0 21
      CI/conan/base/cross-macro.j2
  20. 0 10
      CI/conan/base/cross-windows
  21. 0 5
      CI/conan/base/ios
  22. 0 4
      CI/conan/base/macos
  23. 0 5
      CI/conan/ios-arm64
  24. 0 8
      CI/conan/ios-armv7
  25. 0 5
      CI/conan/macos-arm
  26. 0 5
      CI/conan/macos-intel
  27. 0 15
      CI/conan/mingw32-linux.jinja
  28. 0 13
      CI/conan/mingw64-linux.jinja
  29. 2 1
      CI/emit_partial.py
  30. 1 1
      CI/example.markdownlint-cli2.jsonc
  31. 6 6
      CI/install_conan_dependencies.sh
  32. 0 26
      CI/install_vcpkg_dependencies.sh
  33. 9 0
      CI/wininstaller/download_ucrt.sh
  34. 22 91
      CMakeLists.txt
  35. 47 109
      CMakePresets.json
  36. BIN
      Mods/vcmi/Content/Sprites/adventureLayers.png
  37. 5 0
      Mods/vcmi/Content/config/english.json
  38. 1 20
      android/vcmi-app/build.gradle
  39. 1 1
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java
  40. 0 22
      android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java
  41. 0 650
      android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
  42. 0 679
      android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java
  43. 0 309
      android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
  44. 0 85
      android/vcmi-app/src/main/java/org/libsdl/app/SDL.java
  45. 0 2102
      android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java
  46. 0 394
      android/vcmi-app/src/main/java/org/libsdl/app/SDLAudioManager.java
  47. 0 814
      android/vcmi-app/src/main/java/org/libsdl/app/SDLControllerManager.java
  48. 0 405
      android/vcmi-app/src/main/java/org/libsdl/app/SDLSurface.java
  49. 3 3
      client/CMakeLists.txt
  50. 3 2
      client/GameEngine.cpp
  51. 1 1
      client/GameEngine.h
  52. 2 2
      client/adventureMap/AdventureMapShortcuts.cpp
  53. 1 1
      client/adventureMap/AdventureMapShortcuts.h
  54. 10 3
      client/adventureMap/AdventureMapWidget.cpp
  55. 2 4
      client/eventsSDL/InputHandler.cpp
  56. 1 1
      client/eventsSDL/InputSourceKeyboard.cpp
  57. 1 1
      client/media/CMusicHandler.cpp
  58. 105 0
      client/render/AssetGenerator.cpp
  59. 2 0
      client/render/AssetGenerator.h
  60. 7 0
      client/render/Canvas.cpp
  61. 3 0
      client/render/Canvas.h
  62. 4 0
      client/render/IImage.h
  63. 1 1
      client/render/IScreenHandler.h
  64. 5 0
      client/renderSDL/RenderHandler.cpp
  65. 11 1
      client/renderSDL/SDLImage.cpp
  66. 3 0
      client/renderSDL/SDLImage.h
  67. 16 2
      client/renderSDL/ScreenHandler.cpp
  68. 1 1
      client/renderSDL/ScreenHandler.h
  69. 4 4
      client/windows/CCreatureWindow.cpp
  70. 3 3
      client/windows/settings/GeneralOptionsTab.cpp
  71. 6 16
      clientapp/CMakeLists.txt
  72. 3 3
      cmake_modules/FindSDL2.cmake
  73. 1 1
      cmake_modules/FindSDL2_image.cmake
  74. 1 1
      cmake_modules/FindSDL2_mixer.cmake
  75. 1 1
      cmake_modules/Findminizip.cmake
  76. 28 18
      cmake_modules/VCMIUtils.cmake
  77. 47 344
      conanfile.py
  78. 188 10
      config/bonuses.json
  79. 10 5
      config/creatures/neutral.json
  80. 5 0
      config/schemas/bonus.json
  81. 35 4
      config/widgets/adventureMap.json
  82. 1 0
      dependencies
  83. 17 18
      docker/BuildAndroid-aarch64.dockerfile
  84. 33 19
      docs/developers/Building_Android.md
  85. 110 82
      docs/developers/Building_Windows.md
  86. 4 6
      docs/developers/Building_iOS.md
  87. 23 48
      docs/developers/Building_macOS.md
  88. 184 99
      docs/developers/Conan.md
  89. 6 8
      launcher/CMakeLists.txt
  90. 169 161
      launcher/translation/japanese.ts
  91. 7 0
      lib/CBonusTypeHandler.cpp
  92. 2 0
      lib/CBonusTypeHandler.h
  93. 3 3
      lib/CMakeLists.txt
  94. 12 32
      lib/CStack.cpp
  95. 0 6
      lib/filesystem/MinizipExtensions.h
  96. 0 22
      lib/minizip/CMakeLists.txt
  97. 0 6
      lib/minizip/MiniZip64_Changes.txt
  98. 0 74
      lib/minizip/MiniZip64_info.txt
  99. 0 131
      lib/minizip/crypt.h
  100. 0 247
      lib/minizip/ioapi.c

+ 89 - 97
.github/workflows/github.yml

@@ -17,7 +17,7 @@ jobs:
       matrix:
         include:
           - platform: mac-intel
-            os: macos-13
+            os: macos-14
             pack: 1
             upload: 1
             pack_type: Release
@@ -26,11 +26,11 @@ jobs:
             preset: macos-conan-ninja-release
             conan_profile: macos-intel
             conan_prebuilts: dependencies-mac-intel
-            conan_options: --options with_apple_system_libs=True
+            conan_options: --profile=dependencies/conan_profiles/base/apple-system
             artifact_platform: intel
 
           - platform: mac-arm
-            os: macos-13
+            os: macos-14
             pack: 1
             upload: 1
             pack_type: Release
@@ -39,11 +39,11 @@ jobs:
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
             conan_prebuilts: dependencies-mac-arm
-            conan_options: --options with_apple_system_libs=True
+            conan_options: --profile=dependencies/conan_profiles/base/apple-system
             artifact_platform: arm
 
           - platform: ios
-            os: macos-13
+            os: macos-14
             pack: 1
             upload: 1
             pack_type: Release
@@ -52,7 +52,7 @@ jobs:
             preset: ios-release-conan-ccache
             conan_profile: ios-arm64
             conan_prebuilts: dependencies-ios
-            conan_options: --options with_apple_system_libs=True
+            conan_options: --profile=dependencies/conan_profiles/base/apple-system
 
           - platform: msvc-x64
             arch: x64
@@ -61,8 +61,10 @@ jobs:
             upload: 0
             pack_type: RelWithDebInfo
             extension: zip
-            before_install: msvc.sh
             preset: windows-msvc-ninja-release
+            conan_profile: msvc-x64
+            conan_prebuilts: dependencies-windows-x64
+            conan_options: -s "&:build_type=RelWithDebInfo" -c tools.env.virtualenv:powershell=pwsh -o "&:target_pre_windows10=True"
             artifact_platform: x64
             cl: Hostx64/x64/cl.exe
 
@@ -73,8 +75,10 @@ jobs:
             upload: 0
             pack_type: RelWithDebInfo
             extension: zip
-            before_install: msvc.sh
             preset: windows-msvc-ninja-release-x86
+            conan_profile: msvc-x86
+            conan_prebuilts: dependencies-windows-x86
+            conan_options: -s "&:build_type=RelWithDebInfo" -c tools.env.virtualenv:powershell=pwsh -o "&:target_pre_windows10=True"
             artifact_platform: x86
             cl: Hostx64/x86/cl.exe
 
@@ -85,55 +89,43 @@ jobs:
             upload: 0
             pack_type: RelWithDebInfo
             extension: zip
-            before_install: msvc.sh
             preset: windows-msvc-ninja-release-arm64
+            conan_profile: msvc-arm64
+            conan_prebuilts: dependencies-windows-arm64
+            conan_options: -s "&:build_type=RelWithDebInfo" -c tools.env.virtualenv:powershell=pwsh -o "&:lua_lib=lua"
             artifact_platform: arm64
             cl: HostARM64/ARM64/cl.exe
 
-          - platform: mingw_x86_64
-            arch: x86_64
-            os: ubuntu-24.04
-            pack: 1
-            pack_type: Release
-            extension: zip
-            cmake_args: -G Ninja
-            before_install: mingw.sh
-            preset: windows-mingw-conan-linux
-            conan_profile: mingw64-linux.jinja
-            conan_prebuilts: dependencies-mingw-x86-64
-
-          - platform: mingw_x86
-            arch: x86
-            os: ubuntu-24.04
-            pack: 1
-            pack_type: Release
-            extension: zip
-            cmake_args: -G Ninja
-            before_install: mingw.sh
-            preset: windows-mingw-conan-linux
-            conan_profile: mingw32-linux.jinja
-            conan_prebuilts: dependencies-mingw-x86
-
           - platform: android-32
-            os: ubuntu-24.04
+            os: ubuntu-latest
             upload: 1
             extension: apk
             preset: android-conan-ninja-release
-            before_install: android.sh
             conan_profile: android-32-ndk
             conan_prebuilts: dependencies-android-armeabi-v7a
+            conan_options: --profile=dependencies/conan_profiles/base/android-system
             artifact_platform: armeabi-v7a
 
           - platform: android-64
-            os: ubuntu-24.04
+            os: ubuntu-latest
             upload: 1
             extension: apk
             preset: android-conan-ninja-release
-            before_install: android.sh
             conan_profile: android-64-ndk
             conan_prebuilts: dependencies-android-arm64-v8a
+            conan_options: --profile=dependencies/conan_profiles/base/android-system
             artifact_platform: arm64-v8a
 
+          - platform: android-64-intel
+            os: ubuntu-latest
+            upload: 1
+            extension: apk
+            preset: android-conan-ninja-release
+            conan_profile: android-x64-ndk
+            conan_prebuilts: dependencies-android-x64
+            conan_options: --profile=dependencies/conan_profiles/base/android-system
+            artifact_platform: x64
+
     runs-on: ${{ matrix.os }}
     # Allow non-MSVC builds to fail without failing whole job
     # This keeps pipeline moving so Windows Installer job can still run
@@ -148,11 +140,11 @@ jobs:
       uses: actions/checkout@v5
       with:
         submodules: recursive
-    
+
     - name: Prepare APT staging dir
       if: contains(matrix.os, 'ubuntu')
       run: mkdir -p "$RUNNER_TEMP/apt-cache"
-    
+
     - name: APT cache restore
       if: contains(matrix.os, 'ubuntu')
       id: aptcache
@@ -166,7 +158,7 @@ jobs:
     - name: Prepare CI
       if: "${{ matrix.before_install != '' }}"
       run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}' '${{matrix.arch}}'
-    
+
     # Save only on cache miss, GitHub caches are immutable per key
     - name: APT cache save
       if: contains(matrix.os, 'ubuntu') && steps.aptcache.outputs.cache-hit != 'true'
@@ -177,11 +169,9 @@ jobs:
 
     - name: Install Conan Dependencies
       if: "${{ matrix.conan_prebuilts != '' }}"
-      run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
-
-    - name: Install vcpkg Dependencies
-      if: ${{ startsWith(matrix.platform, 'msvc') }}
-      run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}' 'vcpkg'
+      run: |
+        pipx install conan
+        source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
 
     - name: Setup MSVC Developer Command Prompt
       if: ${{ startsWith(matrix.platform, 'msvc') }}
@@ -206,7 +196,7 @@ jobs:
         max-size: '5G'
         verbose: 2
         job-summary: ""           # <-- disable built-in summary to avoid duplicate block
-        
+
     - name: Setup compiler cache for branch builds
       uses: hendrikmuhs/[email protected]
       if: ${{ github.event.number == '' }}
@@ -220,7 +210,7 @@ jobs:
         max-size: '5G'
         verbose: 2
         job-summary: ""           # <-- disable built-in summary to avoid duplicate block
-        
+
     - name: CCache tuning (Android)
       if: ${{ startsWith(matrix.platform, 'android') }}
       run: |
@@ -230,33 +220,36 @@ jobs:
         ccache --set-config=hash_dir=true
         ccache --set-config=sloppiness=time_macros
 
-    - name: Install Conan
-      if: "${{ matrix.conan_profile != '' }}"
-      run: pipx install 'conan<2.0'
-
     - name: Install Conan profile
       if: "${{ matrix.conan_profile != '' }}"
       run: |
-        conan profile new default --detect
+        conan profile detect
         conan install . \
-          --install-folder=conan-generated \
-          --no-imports \
+          --output-folder=conan-generated \
           --build=never \
-          --profile:build=default \
-          --profile:host=CI/conan/${{ matrix.conan_profile }} \
+          --profile=dependencies/conan_profiles/${{ matrix.conan_profile }} \
           ${{ matrix.conan_options }}
-      env:
-        GENERATE_ONLY_BUILT_CONFIG: 1
 
     # Can't be set in Gradle project
     - name: Configure enableUncompressedNativeLibs
       if: ${{ startsWith(matrix.platform, 'android') }}
       run: mkdir -p ~/.gradle && echo "android.bundle.enableUncompressedNativeLibs=true" > ~/.gradle/gradle.properties
 
-    # Workaround for gradle not discovering SDK that was installed via conan
-    - name: Find Android NDK
+    # Workaround for gradle not discovering SDK that was installed via Сonan
+    - name: Link Android NDK from Conan cache to Android SDK path
       if: ${{ startsWith(matrix.platform, 'android') }}
-      run: sudo ln -s -T /home/runner/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/local/lib/android/sdk/ndk/25.2.9519653
+      run: |
+        ndkPackage='android-ndk'
+        hexRegex='[[:xdigit:]]+'
+
+        ndkPackageRevision=$(conan list "$ndkPackage/*:*" --format=compact \
+          | egrep --only-matching "$ndkPackage/\\w+#$hexRegex:$hexRegex")
+        ndkPackagePath=$(conan cache path "$ndkPackageRevision")
+
+        # format: Pkg.Revision = 25.2.9519653
+        ndkPath="$ndkPackagePath/bin"
+        ndkVersion=$(fgrep 'Pkg.Revision' "$ndkPath/source.properties" | cut -d ' ' -f 3)
+        ln -s -T "$ndkPath" "$ANDROID_HOME/ndk/$ndkVersion"
 
     - name: Install Java
       uses: actions/setup-java@v5
@@ -266,6 +259,7 @@ jobs:
         java-version: '17'
 
     # a hack to build ID for x64 build in order for Google Play to allow upload of both 32 and 64 bit builds
+    # TODO: x86_64
     - name: Bump Android x64 build ID
       if: ${{ matrix.platform == 'android-64' }}
       run: perl -i -pe 's/versionCode (\d+)/$x=$1+1; "versionCode $x"/e' android/vcmi-app/build.gradle
@@ -283,27 +277,33 @@ jobs:
       env:
         PULL_REQUEST: ${{ github.event.pull_request.number }}
 
-    - name: Configure
+    - name: Configure (non-MSVC)
+      if: ${{ !startsWith(matrix.platform, 'msvc') }}
       run: |
         if [[ ("${{ matrix.preset }}" == "android-conan-ninja-release") && ("${{ github.ref }}" != 'refs/heads/master') ]]; then
-          cmake -DENABLE_CCACHE:BOOL=ON \
-                -DCMAKE_C_COMPILER_LAUNCHER=ccache \
-                -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-                -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily;applicationVariant=daily" \
-                --preset ${{ matrix.preset }}
-        elif ${{ startsWith(matrix.platform, 'msvc') }}; then
-            CL="$VCToolsInstallDir/bin/${{ matrix.cl }}"
-            cmake \
-              -D CMAKE_C_COMPILER:FILEPATH="$CL" \
-              -D CMAKE_CXX_COMPILER:FILEPATH="$CL" \
-              --preset ${{ matrix.preset }}
-        else
-          cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
+          # key1=value1;key2=value2;...
+          gradleProperties=$(python3 CI/android/gradle_daily_props.py)
+          androidOptions=("-DANDROID_GRADLE_PROPERTIES=$gradleProperties")
+          androidOptions+=("-DCMAKE_C_COMPILER_LAUNCHER=ccache" "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache")
         fi
+        cmake -DENABLE_CCACHE:BOOL=ON "${androidOptions[@]}" --preset ${{ matrix.preset }}
+    - name: Configure (MSVC)
+      if: ${{ startsWith(matrix.platform, 'msvc') }}
+      run: |
+        & conan-generated\conanrun.ps1
+
+        $CL = "$($env:VCToolsInstallDir)/bin/${{ matrix.cl }}"
+        cmake `
+          -D "CMAKE_C_COMPILER:FILEPATH=$CL" `
+          -D "CMAKE_CXX_COMPILER:FILEPATH=$CL" `
+          --preset ${{ matrix.preset }}
+      shell: pwsh
 
     - name: Build
       run: |
+        ${{ startsWith(matrix.platform, 'msvc') && '& conan-generated\conanrun.ps1' }}
         cmake --build --preset ${{matrix.preset}}
+      shell: pwsh
       env:
         ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
         ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
@@ -320,15 +320,10 @@ jobs:
       id: cpack
       if: ${{ matrix.pack == 1 }}
       run: |
-        cd '${{github.workspace}}/out/build/${{matrix.preset}}'
-        
-        # Workaround for CPack bug on macOS 13
-        counter=0
-        until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do
-            sleep 3
-            ((counter++))
-        done
-        rm -rf _CPack_Packages
+        ${{ startsWith(matrix.platform, 'msvc') && '& conan-generated\conanrun.ps1' }}
+        cd "${{github.workspace}}/out/build/${{matrix.preset}}"
+        cpack -C ${{matrix.pack_type}}
+      shell: pwsh
 
     - name: Find Android package
       if: ${{ startsWith(matrix.platform, 'android') }}
@@ -410,14 +405,14 @@ jobs:
           run: |
             source '${{github.workspace}}/CI/get_package_name.sh'
             echo VCMI_PACKAGE_FILE_NAME="$VCMI_PACKAGE_FILE_NAME" >> $GITHUB_ENV
-            
+
         - name: Create source code archive (including submodules)
           run: |
             git archive HEAD -o "release.tar" --worktree-attributes -v
             git submodule update --init --recursive
             git submodule --quiet foreach 'cd "$toplevel"; tar -rvf "release.tar" "$sm_path"'
             gzip release.tar
-            
+
         - name: Upload source code archive
           id: upload_source
           uses: actions/upload-artifact@v4
@@ -434,7 +429,7 @@ jobs:
             cat > .summary/source.json <<JSON
             {"source_url": "${{ steps.upload_source.outputs.artifact-url }}"}
             JSON
-    
+
         - name: Upload partial JSON with source informations
           uses: actions/upload-artifact@v4
           with:
@@ -477,7 +472,7 @@ jobs:
             compiler_cxx: clang++-13
             compiler_cc: clang-13
             preset: linux-clang-test
-  
+
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -504,7 +499,7 @@ jobs:
           ${{ matrix.platform }}-apt-${{ matrix.os }}
 
     - name: Prepare CI
-      run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}'
+      run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}' '${{matrix.arch}}'
 
     - name: APT cache save
       if: contains(matrix.os, 'ubuntu') && steps.aptcache.outputs.cache-hit != 'true'
@@ -549,7 +544,7 @@ jobs:
         7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
         mkdir -p ~/.local/share/vcmi/
         mv h3_assets/* ~/.local/share/vcmi/
-  
+
     - name: Configure
       run: |
         cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=${{ matrix.compiler_cc }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler_cxx }} --preset ${{ matrix.preset }}
@@ -595,7 +590,6 @@ jobs:
 
     - name: Extract version info
       id: extract-version
-      shell: bash
       run: |
         filePath="${GITHUB_WORKSPACE}/cmake_modules/VersionDefinition.cmake"
 
@@ -609,8 +603,8 @@ jobs:
         echo "short_version=${short_version}" >> "$GITHUB_OUTPUT"
         echo "version_timestamp=${version_timestamp}" >> "$GITHUB_OUTPUT"
 
-    - name: Install ucrt Dependencies
-      run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}' 'ucrt'
+    - name: Download UCRT
+      run: source '${{github.workspace}}/CI/wininstaller/download_ucrt.sh' '${{matrix.platform}}'
 
     - name: Build Number
       run: |
@@ -630,13 +624,11 @@ jobs:
         path: ${{github.workspace}}/artifact
 
     - name: Extract Artifact
-      shell: bash
       run: |
         mkdir artifact/extracted
         unzip "artifact/${{ env.VCMI_PACKAGE_FILE_NAME }}.zip" -d artifact/extracted
 
     - name: Ensure Inno Setup is installed
-      shell: bash
       run: |
         if [ ! -f "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" ] && [ ! -f "/c/ProgramData/Chocolatey/bin/ISCC.exe" ]; then
           choco install innosetup --no-progress -y
@@ -728,7 +720,7 @@ jobs:
     steps:
       - name: Checkout (for script path)
         uses: actions/checkout@v5
-  
+
       - name: Download all partial JSON artifacts
         continue-on-error: true
         uses: actions/download-artifact@v5
@@ -736,7 +728,7 @@ jobs:
           pattern: partial-json-*
           merge-multiple: true
           path: partials
-  
+
       - name: Run final summary
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 44 - 0
.github/workflows/qt_translation_update.yml

@@ -0,0 +1,44 @@
+name: Weekly Qt translation refresh
+
+on:
+  schedule:
+    - cron: '0 0 * * 0'   # Runs every Sunday at 00:00 UTC
+  workflow_dispatch:
+
+permissions:
+  contents: write
+
+jobs:
+  refresh:
+    name: Qt translations refresh
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          ref: ${{ github.ref_name }}
+          fetch-depth: 0
+          submodules: false
+
+      - name: Install Qt tools (lupdate)
+        run: |
+          sudo apt install -y --no-install-recommends qttools5-dev-tools
+
+      - name: Update TS files (Launcher)
+        working-directory: launcher
+        run: |
+          lupdate . -ts translation/*.ts
+
+      - name: Update TS files (Map Editor)
+        working-directory: mapeditor
+        run: |
+          lupdate . -ts translation/*.ts
+
+      - name: Commit and push translation updates
+        uses: EndBug/add-and-commit@v9
+        with:
+          add: "launcher/translation mapeditor/translation"
+          default_author: github_actions
+          message: "Auto-update VCMI Qt translation files"
+          push: true

+ 0 - 2
.gitignore

@@ -56,8 +56,6 @@ fuzzylite.pc
 /client/RD
 /launcher/RD
 /lib/RD
-/lib/minizip/RD
-/lib/minizip/*/RD
 /scripting/erm/RD
 /scripting/erm/*/RD
 /server/RD

+ 3 - 0
.gitmodules

@@ -10,3 +10,6 @@
 	path = launcher/lib/innoextract
 	url = https://github.com/vcmi/innoextract.git
 	branch = vcmi
+[submodule "dependencies"]
+	path = dependencies
+	url = https://github.com/vcmi/vcmi-dependencies.git

+ 0 - 4
AI/CMakeLists.txt

@@ -19,10 +19,6 @@ else()
 	set(fuzzylite_FOUND FALSE)
 endif()
 
-if(TARGET fuzzylite::fuzzylite AND MSVC)
-	install_vcpkg_imported_tgt(fuzzylite::fuzzylite)
-endif()
-
 if(NOT fuzzylite_FOUND)
 	set(FL_BUILD_BINARY OFF CACHE BOOL "")
 	set(FL_BUILD_SHARED OFF CACHE BOOL "")

+ 7 - 0
CI/android/gradle_daily_props.py

@@ -0,0 +1,7 @@
+dic = {
+    "applicationIdSuffix": ".daily",
+    "applicationLabel": "VCMI daily",
+    "applicationVariant": "daily",
+    "signingConfig": "dailySigning",
+}
+print(";".join([f"{key}={value}" for key, value in dic.items()]))

+ 0 - 4
CI/before_install/android.sh

@@ -1,4 +0,0 @@
-#!/bin/sh
-
-sudo apt-get update
-sudo apt-get install ninja-build

+ 1 - 1
CI/before_install/linux_qt5.sh

@@ -24,4 +24,4 @@ sudo eatmydata apt -yq --no-install-recommends \
 
 sudo rm -f  "$APT_CACHE/lock" || true
 sudo rm -rf "$APT_CACHE/partial" || true
-sudo chown -R "$USER:$USER" "$APT_CACHE"
+sudo chown -R "$USER:$USER" "$APT_CACHE"

+ 1 - 1
CI/before_install/linux_qt6.sh

@@ -24,4 +24,4 @@ sudo eatmydata apt -yq --no-install-recommends \
 
 sudo rm -f  "$APT_CACHE/lock" || true
 sudo rm -rf "$APT_CACHE/partial" || true
-sudo chown -R "$USER:$USER" "$APT_CACHE"
+sudo chown -R "$USER:$USER" "$APT_CACHE"

+ 1 - 1
CI/before_install/macos.sh

@@ -1,3 +1,3 @@
 #!/usr/bin/env bash
 
-echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
+echo DEVELOPER_DIR=/Applications/Xcode_16.2.app >> $GITHUB_ENV

+ 0 - 35
CI/before_install/mingw.sh

@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-export DEBIAN_FRONTEND=noninteractive
-
-ARCH="${1:-x86_64}"
-case "$ARCH" in
-  x86)     triplet=i686-w64-mingw32 ;;
-  x86_64)  triplet=x86_64-w64-mingw32 ;;
-  *) echo "Unsupported ARCH '$ARCH' (use: x86 | x86_64)"; exit 2 ;;
-esac
-
-APT_CACHE="${APT_CACHE:-${RUNNER_TEMP:-/tmp}/apt-cache}"
-sudo mkdir -p "$APT_CACHE"
-
-sudo apt -yq -o Acquire::Retries=3 update
-sudo apt -yq install eatmydata
-
-sudo eatmydata apt -yq --no-install-recommends \
-  -o Dir::Cache::archives="$APT_CACHE" \
-  -o APT::Keep-Downloaded-Packages=true \
-  -o Acquire::Retries=3 -o Dpkg::Use-Pty=0 \
-  install \
-  ninja-build nsis mingw-w64 g++-mingw-w64
-
-if [[ -x "/usr/bin/${triplet}-g++-posix" ]]; then
-  sudo update-alternatives --set "${triplet}-g++" "/usr/bin/${triplet}-g++-posix"
-fi
-if [[ -x "/usr/bin/${triplet}-gcc-posix" ]]; then
-  sudo update-alternatives --set "${triplet}-gcc" "/usr/bin/${triplet}-gcc-posix"
-fi
-
-sudo rm -f  "$APT_CACHE/lock" || true
-sudo rm -rf "$APT_CACHE/partial" || true
-sudo chown -R "$USER:$USER" "$APT_CACHE"

+ 0 - 17
CI/before_install/msvc.sh

@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-MSVC_INSTALL_PATH=$(vswhere -latest -property installationPath)
-echo "MSVC_INSTALL_PATH = $MSVC_INSTALL_PATH"
-echo "Installed toolset versions:"
-ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC"
-
-TOOLS_DIR=$(ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC/" | head -1)
-DUMPBIN_PATH="$MSVC_INSTALL_PATH/VC/Tools/MSVC/$TOOLS_DIR/bin/Hostx64/x64/dumpbin.exe"
-
-# This command should work as well, but for some reason it is *extremely* slow on the Github CI (~7 minutes)
-#DUMPBIN_PATH=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
-
-echo "TOOLS_DIR = $TOOLS_DIR"
-echo "DUMPBIN_PATH = $DUMPBIN_PATH"
-
-dirname "$DUMPBIN_PATH" > "$GITHUB_PATH"

+ 0 - 5
CI/conan/android-32

@@ -1,5 +0,0 @@
-include(base/android)
-
-[settings]
-arch=armv7
-os.api_level=19

+ 0 - 4
CI/conan/android-32-ndk

@@ -1,4 +0,0 @@
-include(android-32)
-
-[tool_requires]
-android-ndk/r25c

+ 0 - 5
CI/conan/android-64

@@ -1,5 +0,0 @@
-include(base/android)
-
-[settings]
-arch=armv8
-os.api_level=21

+ 0 - 4
CI/conan/android-64-ndk

@@ -1,4 +0,0 @@
-include(android-64)
-
-[tool_requires]
-android-ndk/r25c

+ 0 - 10
CI/conan/base/android

@@ -1,10 +0,0 @@
-[settings]
-build_type=Release
-compiler=clang
-compiler.libcxx=c++_shared
-compiler.version=14
-os=Android
-
-[buildenv]
-# fixes shared libiconv build
-LD=ld

+ 0 - 12
CI/conan/base/apple

@@ -1,12 +0,0 @@
-[settings]
-compiler=apple-clang
-compiler.version=14
-compiler.libcxx=libc++
-build_type=Release
-
-# required for Boost.Locale in versions >= 1.81
-compiler.cppstd=11
-
-[conf]
-tools.apple:enable_bitcode = False
-tools.cmake.cmaketoolchain:generator = Ninja

+ 0 - 21
CI/conan/base/cross-macro.j2

@@ -1,21 +0,0 @@
-{% macro generate_env(target_host) -%}
-CONAN_CROSS_COMPILE={{ target_host }}-
-CHOST={{ target_host }}
-AR={{ target_host }}-ar
-AS={{ target_host }}-as
-CC={{ target_host }}-gcc
-CXX={{ target_host }}-g++
-RANLIB={{ target_host }}-ranlib
-STRIP={{ target_host }}-strip
-{%- endmacro -%}
-
-{% macro generate_env_win32(target_host) -%}
-CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/13-posix/
-RC={{ target_host }}-windres
-{%- endmacro -%}
-
-{% macro generate_conf(target_host) -%}
-tools.build:compiler_executables = {"c": "{{ target_host }}-gcc", "cpp": "{{ target_host }}-g++"}
-tools.build:sysroot = /usr/{{ target_host }}
-tools.build:defines = ["WINVER=0x0601", "_WIN32_WINNT=0x0601"]
-{%- endmacro -%}

+ 0 - 10
CI/conan/base/cross-windows

@@ -1,10 +0,0 @@
-[settings]
-os=Windows
-compiler=gcc
-compiler.libcxx=libstdc++11
-compiler.version=10
-compiler.cppstd=11
-build_type=Release
-
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 0 - 5
CI/conan/base/ios

@@ -1,5 +0,0 @@
-include(apple)
-
-[settings]
-os=iOS
-os.sdk=iphoneos

+ 0 - 4
CI/conan/base/macos

@@ -1,4 +0,0 @@
-include(apple)
-
-[settings]
-os=Macos

+ 0 - 5
CI/conan/ios-arm64

@@ -1,5 +0,0 @@
-include(base/ios)
-
-[settings]
-os.version=12.0
-arch=armv8

+ 0 - 8
CI/conan/ios-armv7

@@ -1,8 +0,0 @@
-include(base/ios)
-
-[settings]
-os.version=10.0
-arch=armv7
-
-# Xcode 13.x is the last version that can build for armv7
-compiler.version=13

+ 0 - 5
CI/conan/macos-arm

@@ -1,5 +0,0 @@
-include(base/macos)
-
-[settings]
-os.version=11.0
-arch=armv8

+ 0 - 5
CI/conan/macos-intel

@@ -1,5 +0,0 @@
-include(base/macos)
-
-[settings]
-os.version=10.13
-arch=x86_64

+ 0 - 15
CI/conan/mingw32-linux.jinja

@@ -1,15 +0,0 @@
-{% import 'base/cross-macro.j2' as cross -%}
-include(base/cross-windows)
-{% set target_host="i686-w64-mingw32" %}
-
-[settings]
-arch=x86
-
-[conf]
-{{ cross.generate_conf(target_host)}}
-tools.build:cflags = ["-msse2"]
-tools.build:cxxflags = ["-msse2"]
-
-[env]
-{{ cross.generate_env(target_host) }}
-{{ cross.generate_env_win32(target_host) }}

+ 0 - 13
CI/conan/mingw64-linux.jinja

@@ -1,13 +0,0 @@
-{% import 'base/cross-macro.j2' as cross -%}
-include(base/cross-windows)
-{% set target_host="x86_64-w64-mingw32" %}
-
-[settings]
-arch=x86_64
-
-[conf]
-{{ cross.generate_conf(target_host)}}
-
-[env]
-{{ cross.generate_env(target_host) }}
-{{ cross.generate_env_win32(target_host) }}

+ 2 - 1
CI/emit_partial.py

@@ -83,7 +83,7 @@ def arch_label(platform: str, arch_env: Optional[str]) -> str:
         return arch_env
     mapping = {
         "mac-intel": "Intel",
-        "mac-arm": "ARM64",
+        "mac-arm": "Apple Silicon",
         "ios": "ARM64",
         "msvc-x64": "x64",
         "msvc-x86": "x86",
@@ -92,6 +92,7 @@ def arch_label(platform: str, arch_env: Optional[str]) -> str:
         "mingw_x86_64": "x64",
         "android-32": "ARMv7",
         "android-64": "ARM64",
+        "android-64-intel": "x86_64",
     }
     return mapping.get(platform, platform)
 

+ 1 - 1
CI/example.markdownlint-cli2.jsonc

@@ -184,7 +184,7 @@
 		// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
 		"MD040": {
 		// List of languages
-			"allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ],
+			"allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua", "batchfile" ],
 		// Require language only
 			"language_only": true
 		},

+ 6 - 6
CI/install_conan_dependencies.sh

@@ -1,9 +1,9 @@
 #!/usr/bin/env bash
 
-RELEASE_TAG="1.3"
-FILENAME="$1"
-DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME.txz"
+RELEASE_TAG="2025-08-24"
+FILENAME="$1.tgz"
+DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME"
 
-mkdir ~/.conan
-cd ~/.conan
-curl -L "$DOWNLOAD_URL" | tar -xf - --xz
+downloadedFile="$RUNNER_TEMP/$FILENAME"
+curl -Lo "$downloadedFile" "$DOWNLOAD_URL"
+conan cache restore "$downloadedFile"

+ 0 - 26
CI/install_vcpkg_dependencies.sh

@@ -1,26 +0,0 @@
-#!/usr/bin/env bash
-
-ACCOUNT="vcmi"
-
-# Fetch latest release tag from GitHub API
-# RELEASE_TAG=$(curl -s "https://api.github.com/repos/$ACCOUNT/vcmi-deps-windows/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
-
-RELEASE_TAG="v1.9"
-
-# 2. parameter: all | vcpkg | ucrt  (default: all)
-PART="${2:-all}"
-
-# --- VCPKG ---
-if [[ "$PART" == "all" || "$PART" == "vcpkg"  ]]; then
-  DEP_FILENAME="dependencies-$1"
-  DEP_URL="https://github.com/$ACCOUNT/vcmi-deps-windows/releases/download/$RELEASE_TAG/$DEP_FILENAME.txz"
-  curl -L "$DEP_URL" | tar -xf - --xz
-fi
-
-# --- UCRT  ---
-if [[ "$PART" == "all" || "$PART" == "ucrt" ]]; then
-  UCRT_FILENAME="ucrtRedist-$1"
-  UCRT_URL="https://github.com/$ACCOUNT/vcmi-deps-windows/releases/download/$RELEASE_TAG/$UCRT_FILENAME.txz"
-  mkdir -p ucrt
-  curl -L "$UCRT_URL" | tar -xf - --xz -C ucrt
-fi

+ 9 - 0
CI/wininstaller/download_ucrt.sh

@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+RELEASE_TAG="v1.9"
+UCRT_FILENAME="ucrtRedist-$1"
+UCRT_URL="https://github.com/vcmi/vcmi-deps-windows/releases/download/$RELEASE_TAG/$UCRT_FILENAME.txz"
+
+UCRT_DIR="ucrt"
+mkdir -p "$UCRT_DIR"
+curl -L "$UCRT_URL" | tar -xf - --xz -C "$UCRT_DIR"

+ 22 - 91
CMakeLists.txt

@@ -17,12 +17,8 @@ project(VCMI)
 # It's used currently to make sure that 3rd-party dependencies in git submodules get proper FOLDER property
 # - Make FindFuzzyLite check for the right version and disable FORCE_BUNDLED_FL by default
 
-if(APPLE)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
-		set(APPLE_MACOS 1)
-	else()
-		set(APPLE_IOS 1)
-	endif()
+if(APPLE AND NOT IOS)
+	set(MACOS 1)
 endif()
 
 if(POLICY CMP0142)
@@ -77,12 +73,12 @@ else()
 	option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
 endif()
 
-if(APPLE_IOS)
+if(IOS)
 	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
 	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
 endif()
 
-if(APPLE_IOS OR ANDROID)
+if(IOS OR ANDROID)
 	set(ENABLE_MONOLITHIC_INSTALL OFF)
 	set(ENABLE_SINGLE_APP_BUILD ON)
 	set(ENABLE_EDITOR OFF)
@@ -155,7 +151,7 @@ if(ENABLE_CCACHE)
     endif()
 endif()
 
-if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode"))
+if(ENABLE_CCACHE AND XCODE)
 	# https://stackoverflow.com/a/36515503/2278742
 	# Set up wrapper scripts
 	configure_file(xcode/launch-c.in   xcode/launch-c)
@@ -183,7 +179,7 @@ include(VersionDefinition)
 vcmi_print_important_variables()
 
 # Options to enable folders in CMake generated projects for Visual Studio, Xcode, etc
-# Very useful to put 3rd-party libraries such as Minizip, GoogleTest and FuzzyLite in their own folders
+# Very useful to put 3rd-party libraries such as GoogleTest and FuzzyLite in their own folders
 set_property(GLOBAL PROPERTY USE_FOLDERS TRUE)
 # Make FOLDER property inheritable
 # So when we set FOLDER property on AI directory all and targets inside will inherit it
@@ -287,7 +283,7 @@ if(ENABLE_GOLDMASTER)
 	add_definitions(-DENABLE_GOLDMASTER)
 endif()
 
-if(APPLE_IOS)
+if(IOS)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
 
@@ -303,14 +299,14 @@ if(APPLE_IOS)
 	set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2")
 endif()
 
-if(APPLE_MACOS)
+if(MACOS)
 	# Not supported by standard library of our current minimal target - macOS 10.14 or newer required
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-aligned-allocation")
 endif()
 
 if(MINGW OR MSVC)
 	# Windows Vista or newer for FuzzyLite 6 to compile
-	# Except for conan which already has this definition in its preset
+	# Except for conan which already has this definition in its profile
 	if(NOT USING_CONAN)
 		add_definitions(-D_WIN32_WINNT=0x0600)
 	endif()
@@ -373,14 +369,9 @@ if(MINGW OR MSVC)
 		# Reported to Microsoft here:
 		# https://developercommunity.visualstudio.com/content/problem/224597/linker-failing-because-of-multiple-definitions-of.html
 		set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
-
-		# Required at least for compatibility with Boost 1.68 on Vcpkg
-  		# psapi required for ARM64 builds
-		set(SYSTEM_LIBS ${SYSTEM_LIBS} bcrypt psapi)
 	endif(MSVC)
 
 	if(MINGW)
-
 		# Temporary (?) workaround for failing builds on MinGW CI due to bug in TBB
 		set(CMAKE_CXX_EXTENSIONS ON)
 
@@ -446,7 +437,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32)
 endif()
 
 # Check if some platform-specific libraries are needed for linking
-if(NOT WIN32 AND NOT APPLE_IOS)
+if(NOT WIN32 AND NOT IOS)
 	include(CheckLibraryExists)
 
 	# Shared memory functions used by Boost.Interprocess
@@ -505,14 +496,7 @@ if(TARGET zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 endif()
 
-option(FORCE_BUNDLED_MINIZIP "Force bundled Minizip library" OFF)
-if(NOT FORCE_BUNDLED_MINIZIP)
-	find_package(minizip)
-	if(TARGET minizip::minizip)
-		add_definitions(-DUSE_SYSTEM_MINIZIP)
-	endif()
-endif()
-
+find_package(minizip REQUIRED)
 
 if (ENABLE_CLIENT)
 	if (ENABLE_VIDEO)
@@ -608,7 +592,7 @@ elseif(APPLE)
 		set(LIB_DIR "." CACHE STRING "Where to install main library")
 		set(DATA_DIR "." CACHE STRING "Where to install data files")
 	else()
-		if(APPLE_MACOS)
+		if(MACOS)
 			set(APP_BUNDLE_DIR "${CMAKE_PROJECT_NAME}.app")
 			set(APP_BUNDLE_CONTENTS_DIR "${APP_BUNDLE_DIR}/Contents")
 			set(APP_BUNDLE_BINARY_DIR "${APP_BUNDLE_CONTENTS_DIR}/MacOS")
@@ -626,6 +610,9 @@ elseif(ANDROID)
 	include(GNUInstallDirs)
 	set(LIB_DIR "libs/${ANDROID_ABI}")
 
+	file(READ "${CMAKE_ANDROID_NDK}/meta/abis.json" ndkAbiInfo)
+	string(JSON ANDROID_SYSROOT_LIB_SUBDIR GET "${ndkAbiInfo}" "${ANDROID_ABI}" "triple")
+
 	# required by Qt
 	set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android")
 	set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build")
@@ -666,7 +653,7 @@ else()
 endif()
 
 # iOS/Android have flat libs directory structure
-if(APPLE_IOS OR ANDROID)
+if(IOS OR ANDROID)
 	set(AI_LIB_DIR "${LIB_DIR}")
 	set(SCRIPTING_LIB_DIR "${LIB_DIR}")
 else()
@@ -685,7 +672,7 @@ endif()
 #        Add subdirectories           #
 #######################################
 
-if(APPLE_IOS)
+if(IOS)
 	add_subdirectory(ios)
 endif()
 
@@ -707,11 +694,6 @@ if(ENABLE_LUA)
 	add_subdirectory(scripting/lua)
 endif()
 
-if(NOT TARGET minizip::minizip)
-	add_subdirectory_with_folder("3rdparty" lib/minizip)
-	add_library(minizip::minizip ALIAS minizip)
-endif()
-
 if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	add_subdirectory(vcmiqt)
 endif()
@@ -746,6 +728,8 @@ endif()
 #        Installation section         #
 #######################################
 
+vcmi_install_conan_deps()
+
 if(ANDROID)
 	string(REPLACE ";" "\n" ANDROID_GRADLE_PROPERTIES_MULTILINE "${ANDROID_GRADLE_PROPERTIES}")
 	file(WRITE "${androidPackageSourceDir}/vcmi-app/gradle.properties" "signingRoot=${CMAKE_SOURCE_DIR}/CI/android\n${ANDROID_GRADLE_PROPERTIES_MULTILINE}")
@@ -767,66 +751,13 @@ if(ENABLE_LUA)
 endif()
 
 # that script is useless for Windows / iOS / Android
-if(NOT WIN32 AND NOT APPLE_IOS AND NOT ANDROID)
+if(NOT WIN32 AND NOT IOS AND NOT ANDROID)
 	install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS
 		OWNER_WRITE OWNER_READ OWNER_EXECUTE
 					GROUP_READ GROUP_EXECUTE
 					WORLD_READ WORLD_EXECUTE)
 endif()
 
-
-if(WIN32)
-	if(TBB_FOUND AND MSVC)
-		install_vcpkg_imported_tgt(TBB::tbb)
-	endif()
-
-	if(USING_CONAN)
-		#Conan imports enabled
-		vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
-		file(GLOB dep_files
-				${dep_files}
-				"${CMAKE_SYSROOT}/bin/*.dll" 
-				"${CMAKE_SYSROOT}/lib/*.dll" 
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_dw2-1.dll" # for 32-bit only?
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_seh-1.dll" # for 64-bit only?
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libstdc++-6.dll")
-	else()
-		file(GLOB dep_files
-				${dep_files}
-				"${CMAKE_FIND_ROOT_PATH}/bin/*.dll")
-	endif()
-
-	if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-		# Copy debug versions of libraries if build type is debug
-		set(debug_postfix d)
-	endif()
-
-	if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
-		get_target_property(QtCore_location Qt${QT_VERSION_MAJOR}::Core LOCATION)
-		get_filename_component(Qtbin_folder ${QtCore_location} PATH)
-		file(GLOB dep_files
-			${dep_files}
-			${Qtbin_folder}/Qt5Core${debug_postfix}.dll
-			${Qtbin_folder}/Qt5Gui${debug_postfix}.dll
-			${Qtbin_folder}/Qt5Widgets${debug_postfix}.dll
-			${Qtbin_folder}/Qt5Network${debug_postfix}.dll
-			${Qtbin_folder}/icu*.dll)
-		get_target_property(integration_type Qt${QT_VERSION_MAJOR}::QWindowsIntegrationPlugin TYPE)
-		if(NOT(integration_type STREQUAL "INTERFACE_LIBRARY"))
-			get_target_property(integration_loc Qt${QT_VERSION_MAJOR}::QWindowsIntegrationPlugin LOCATION)
-			install(
-				FILES ${integration_loc}
-				DESTINATION ${BIN_DIR}/platforms
-			)
-			install(
-				FILES "$<TARGET_FILE:Qt${QT_VERSION_MAJOR}::QWindowsVistaStylePlugin>" 
-				DESTINATION ${BIN_DIR}/styles) 
-		endif()
-	endif()
-
-	install(FILES ${dep_files} DESTINATION ${BIN_DIR})
-endif(WIN32)
-
 #######################################
 #       Packaging section             #
 #######################################
@@ -920,7 +851,7 @@ if(WIN32)
 	if(NOT (${CMAKE_CROSSCOMPILING}))
 		add_subdirectory(win)
 	endif()
-elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
+elseif(MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 	set(CPACK_MONOLITHIC_INSTALL 1)
 	set(CPACK_GENERATOR "DragNDrop")
 	set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png")
@@ -964,7 +895,7 @@ elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 
 	# Bundle fixing code must be in separate directory to be executed after all other install code
 	add_subdirectory(osx)
-elseif(APPLE_IOS)
+elseif(IOS)
 	set(CPACK_GENERATOR ZIP)
 	set(CPACK_ARCHIVE_FILE_EXTENSION ipa)
 	set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)

+ 47 - 109
CMakePresets.json

@@ -10,8 +10,7 @@
             "name": "build-with-conan",
             "hidden": true,
             "cacheVariables": {
-                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/conan-generated/conan_toolchain.cmake",
-                "FORCE_BUNDLED_MINIZIP": "OFF"
+                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/conan-generated/conan_toolchain.cmake"
             }
         },
         {
@@ -155,28 +154,30 @@
             "name": "windows-msvc-release",
             "displayName": "Windows x64 RelWithDebInfo",
             "description": "VCMI RelWithDebInfo build",
-            "inherits": "default-release",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
             "generator": "Visual Studio 17 2022",
             "architecture": {
                 "value": "x64",
                 "strategy": "set"
             },
             "cacheVariables": {
-                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
-                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
-                "FORCE_BUNDLED_MINIZIP": "ON"
+                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW"
             }
         },
         {
             "name": "windows-msvc-release-x86",
             "displayName": "Windows x86 RelWithDebInfo",
             "description": "VCMI RelWithDebInfo build",
-            "inherits": "default-release",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
             "generator": "Visual Studio 17 2022",
             "cacheVariables": {
-                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
                 "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
-                "FORCE_BUNDLED_MINIZIP": "ON",
                 "CMAKE_GENERATOR_PLATFORM": "WIN32"
             }
         },
@@ -184,16 +185,17 @@
             "name": "windows-msvc-release-arm64",
             "displayName": "Windows ARM64 RelWithDebInfo",
             "description": "VCMI Windows ARM64 build",
-            "inherits": "default-release",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
             "generator": "Visual Studio 17 2022",
             "architecture": {
                 "value": "ARM64",
                 "strategy": "set"
             },
             "cacheVariables": {
-                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
-                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
-                "FORCE_BUNDLED_MINIZIP": "ON"
+                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW"
             }
         },
         {
@@ -206,57 +208,38 @@
             }
         },
         {
-          "name": "windows-msvc-ninja-release",
-          "displayName": "Windows x64 RelWithDebInfo (Ninja)",
-          "description": "VCMI RelWithDebInfo build using Ninja + sccache",
-          "inherits": "default-release",
-          "generator": "Ninja",
-          "cacheVariables": {
-            "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
-            "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
-            "FORCE_BUNDLED_MINIZIP": "ON",
-            "ENABLE_CCACHE": "ON",
-            "CMAKE_C_COMPILER_LAUNCHER": "sccache",
-            "CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
-            "CMAKE_MSVC_DEBUG_INFORMATION_FORMAT": "Embedded",
-            "ENABLE_MULTI_PROCESS_BUILDS": "OFF",
-            "CMAKE_BUILD_TYPE": "RelWithDebInfo",
-            "CMAKE_C_FLAGS_RELWITHDEBINFO": "/O2 /Ob1 /DNDEBUG /Z7",
-            "CMAKE_CXX_FLAGS_RELWITHDEBINFO": "/O2 /Ob1 /DNDEBUG /Z7"
-          }
-        },
-        {
-          "name": "windows-msvc-ninja-release-x86",
-          "displayName": "Windows x86 RelWithDebInfo (Ninja)",
-          "description": "VCMI RelWithDebInfo build using Ninja + sccache (x86)",
-          "inherits": "windows-msvc-ninja-release",
-          "cacheVariables": {
-            "VCPKG_TARGET_TRIPLET": "x86-windows"
-          }
-        },
-        {
-          "name": "windows-msvc-ninja-release-arm64",
-          "displayName": "Windows ARM64 RelWithDebInfo (Ninja)",
-          "description": "VCMI RelWithDebInfo build using Ninja + sccache (ARM64)",
-          "inherits": "windows-msvc-ninja-release",
-          "cacheVariables": {
-            "VCPKG_TARGET_TRIPLET": "arm64-windows"
-          }
-        },
-        {
-            "name": "windows-mingw-conan-linux",
-            "displayName": "Ninja+Conan release",
-            "description": "VCMI Windows Ninja using Conan on Linux",
+            "name": "windows-msvc-ninja-release",
+            "displayName": "Windows x64 RelWithDebInfo (Ninja)",
+            "description": "VCMI RelWithDebInfo build using Ninja + sccache",
             "inherits": [
                 "build-with-conan",
                 "default-release"
             ],
+            "generator": "Ninja",
             "cacheVariables": {
-                "CMAKE_BUILD_TYPE": "Release",
-                "FORCE_BUNDLED_FL": "ON",
-                "ENABLE_TEMPLATE_EDITOR": "OFF"
+                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
+                "ENABLE_CCACHE": "ON",
+                "CMAKE_C_COMPILER_LAUNCHER": "sccache",
+                "CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
+                "CMAKE_MSVC_DEBUG_INFORMATION_FORMAT": "Embedded",
+                "ENABLE_MULTI_PROCESS_BUILDS": "OFF",
+                "CMAKE_BUILD_TYPE": "RelWithDebInfo",
+                "CMAKE_C_FLAGS_RELWITHDEBINFO": "/O2 /Ob1 /DNDEBUG /Z7",
+                "CMAKE_CXX_FLAGS_RELWITHDEBINFO": "/O2 /Ob1 /DNDEBUG /Z7"
             }
         },
+        {
+            "name": "windows-msvc-ninja-release-x86",
+            "displayName": "Windows x86 RelWithDebInfo (Ninja)",
+            "description": "VCMI RelWithDebInfo build using Ninja + sccache (x86)",
+            "inherits": "windows-msvc-ninja-release"
+        },
+        {
+            "name": "windows-msvc-ninja-release-arm64",
+            "displayName": "Windows ARM64 RelWithDebInfo (Ninja)",
+            "description": "VCMI RelWithDebInfo build using Ninja + sccache (ARM64)",
+            "inherits": "windows-msvc-ninja-release"
+        },
         {
             "name": "macos-ninja-release",
             "displayName": "Ninja release",
@@ -272,8 +255,7 @@
                 "default-release"
             ],
             "cacheVariables": {
-                "CMAKE_BUILD_TYPE": "Release",
-                "ENABLE_TEMPLATE_EDITOR": "OFF"
+                "CMAKE_BUILD_TYPE": "Release"
             }
         },
         {
@@ -290,36 +272,20 @@
             "generator": "Xcode"
         },
         {
-            "name": "ios-device",
-            "displayName": "Base iOS device",
-            "description": "Base VCMI preset for iOS device",
+            "name": "ios-device-conan",
+            "displayName": "Base iOS device using Conan",
+            "description": "Base VCMI preset for iOS device using Conan",
+            "inherits": [
+                "build-with-conan"
+            ],
             "generator": "Xcode",
             "binaryDir": "../build-${presetName}",
             "cacheVariables": {
                 "CMAKE_SYSTEM_NAME": "iOS",
                 "FORCE_BUNDLED_FL": "ON",
-                "FORCE_BUNDLED_MINIZIP": "ON",
                 "ENABLE_EDITOR" : "OFF"
             }
         },
-        {
-            "name": "ios-simulator",
-            "displayName": "Base iOS simulator",
-            "description": "Base VCMI preset for iOS simulator",
-            "inherits": "ios-device",
-            "cacheVariables": {
-                "CMAKE_OSX_SYSROOT": "iphonesimulator"
-            }
-        },
-        {
-            "name": "ios-device-conan",
-            "displayName": "Base iOS device using Conan",
-            "description": "Base VCMI preset for iOS device using Conan",
-            "inherits": [
-                "build-with-conan",
-                "ios-device"
-            ]
-        },
         {
             "name": "base-ios-release",
             "displayName": "Base iOS release",
@@ -352,18 +318,6 @@
                 "ENABLE_CCACHE": "ON"
             }
         },
-        {
-            "name": "ios-release-legacy",
-            "displayName": "iOS release using legacy dependencies",
-            "description": "VCMI iOS release using legacy dependencies",
-            "inherits": [
-                "base-ios-release",
-                "ios-device"
-            ],
-            "cacheVariables": {
-                "CMAKE_PREFIX_PATH": "${sourceDir}/build/iphoneos"
-            }
-        },
         {
             "name": "android-conan-ninja-release",
             "displayName": "Android release",
@@ -522,12 +476,6 @@
         	"configurePreset": "windows-msvc-ninja-release-arm64",
         	"inherits": "default-release"
         },
-        {
-            "name": "windows-mingw-conan-linux",
-            "configurePreset": "windows-mingw-conan-linux",
-            "inherits": "default-release",
-            "configuration": "Release"
-        },
         {
             "name": "ios-release-conan",
             "configurePreset": "ios-release-conan",
@@ -543,11 +491,6 @@
             "configurePreset": "ios-release-conan-ccache",
             "inherits": "ios-release-conan"
         },
-        {
-            "name": "ios-release-legacy",
-            "configurePreset": "ios-release-legacy",
-            "inherits": "ios-release-conan"
-        },
         {
             "name": "android-conan-ninja-release",
             "configurePreset": "android-conan-ninja-release",
@@ -613,11 +556,6 @@
             "name": "windows-msvc-release",
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
-        },
-        {
-            "name": "windows-mingw-conan-linux",
-            "configurePreset": "windows-mingw-conan-linux",
-            "inherits": "default-release"
         }
     ]
 }

BIN
Mods/vcmi/Content/Sprites/adventureLayers.png


+ 5 - 0
Mods/vcmi/Content/config/english.json

@@ -952,6 +952,11 @@
 	"creatures.core.marksman.bonus.extraAttack" : "{Shoots twice}\nThis unit can shoot twice",
 	"creatures.core.azureDragon.bonus.fearful" : "{Fear}\nEnemy units have a 10% chance of freezing in fear",
 	"creatures.core.azureDragon.bonus.fearless" : "{Fearless}\nImmune to Fear ability",
+	"creatures.core.halfling.bonus.lucky" : "{Lucky}\nHalfling's luck cannot be decreased below +1",
+	"creatures.core.nomad.bonus.sandWalker" : "{Sand walker}\nHero ignores terrain penalty on sand",
+	"creatures.core.rogue.bonus.visionsMonsters" : "{Visions Monsters}\nHero can see detailed informations about neutral monsters",
+	"creatures.core.rogue.bonus.visionsHeroes" : "{Visions Heroes}\nHero can see detailed informations about enemy heroes",
+	"creatures.core.rogue.bonus.visionsTowns" : "{Visions Towns}\nHero can see detailed informations about enemy towns",
 
 	"core.bonus.ADDITIONAL_ATTACK.description" : "{Additional attacks}\nUnit can attack an additional {$val} times", // TODO: alternative descriptions for melee/ranged effect range
 	"core.bonus.ADDITIONAL_RETALIATION.description" : "{Additional retaliations}\nUnit can retaliate ${val} extra times",

+ 1 - 20
android/vcmi-app/build.gradle

@@ -46,7 +46,7 @@ android {
 			manifest.srcFile '../AndroidManifest.xml'
 			jniLibs.srcDirs = ['../libs']
 
-			java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
+			java.srcDirs = [qt5AndroidDir + '/src', 'src', SDL_JAVA_SRC_DIR]
 			aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
 			res.srcDirs = [qt5AndroidDir + '/res', 'src/main/res', '../res']
 		}
@@ -102,24 +102,6 @@ android {
 	}
 }
 
-def CommandOutput(final cmd, final arguments, final cwd) {
-	try {
-		new ByteArrayOutputStream().withStream { final os ->
-			exec {
-				executable cmd
-				args arguments
-				workingDir cwd
-				standardOutput os
-			}
-			return os.toString().trim()
-		}
-	}
-	catch (final Exception ex) {
-		print("Broken: " + cmd + " " + arguments + " in " + cwd + " :: " + ex.toString())
-		return ""
-	}
-}
-
 def SigningPropertiesPath(final basePath, final signingConfigKey) {
 	return file("${basePath}/${signingConfigKey}.properties")
 }
@@ -142,7 +124,6 @@ def LoadSigningConfig(final signingConfigKey) {
 			&& props.containsKey('KEY_ALIAS')) {
 
 			signingConfig.storeFile = SigningKeystorePath(signingRoot, props['STORE_FILE'])
-			signingConfig.storePassword = props['STORE_PASSWORD']
 			signingConfig.keyAlias = props['KEY_ALIAS']
 
 			if(props.containsKey('STORE_PASSWORD'))

+ 1 - 1
android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java

@@ -77,7 +77,7 @@ public class VcmiSDLActivity extends SDLActivity
 
     @Override
     protected String[] getLibraries() {
-        // SDL is linked statically, no need to load anything
+        // app main library and SDL are loaded when launcher starts, no extra work to do
         return new String[] {
         };
     }

+ 0 - 22
android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java

@@ -1,22 +0,0 @@
-package org.libsdl.app;
-
-import android.hardware.usb.UsbDevice;
-
-interface HIDDevice
-{
-    public int getId();
-    public int getVendorId();
-    public int getProductId();
-    public String getSerialNumber();
-    public int getVersion();
-    public String getManufacturerName();
-    public String getProductName();
-    public UsbDevice getDevice();
-    public boolean open();
-    public int sendFeatureReport(byte[] report);
-    public int sendOutputReport(byte[] report);
-    public boolean getFeatureReport(byte[] report);
-    public void setFrozen(boolean frozen);
-    public void close();
-    public void shutdown();
-}

+ 0 - 650
android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java

@@ -1,650 +0,0 @@
-package org.libsdl.app;
-
-import android.content.Context;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothGattService;
-import android.hardware.usb.UsbDevice;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.os.*;
-
-//import com.android.internal.util.HexDump;
-
-import java.lang.Runnable;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.UUID;
-
-class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
-
-    private static final String TAG = "hidapi";
-    private HIDDeviceManager mManager;
-    private BluetoothDevice mDevice;
-    private int mDeviceId;
-    private BluetoothGatt mGatt;
-    private boolean mIsRegistered = false;
-    private boolean mIsConnected = false;
-    private boolean mIsChromebook = false;
-    private boolean mIsReconnecting = false;
-    private boolean mFrozen = false;
-    private LinkedList<GattOperation> mOperations;
-    GattOperation mCurrentOperation = null;
-    private Handler mHandler;
-
-    private static final int TRANSPORT_AUTO = 0;
-    private static final int TRANSPORT_BREDR = 1;
-    private static final int TRANSPORT_LE = 2;
-
-    private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
-
-    static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
-    static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
-    static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
-    static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
-
-    static class GattOperation {
-        private enum Operation {
-            CHR_READ,
-            CHR_WRITE,
-            ENABLE_NOTIFICATION
-        }
-
-        Operation mOp;
-        UUID mUuid;
-        byte[] mValue;
-        BluetoothGatt mGatt;
-        boolean mResult = true;
-
-        private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
-            mGatt = gatt;
-            mOp = operation;
-            mUuid = uuid;
-        }
-
-        private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
-            mGatt = gatt;
-            mOp = operation;
-            mUuid = uuid;
-            mValue = value;
-        }
-
-        public void run() {
-            // This is executed in main thread
-            BluetoothGattCharacteristic chr;
-
-            switch (mOp) {
-                case CHR_READ:
-                    chr = getCharacteristic(mUuid);
-                    //Log.v(TAG, "Reading characteristic " + chr.getUuid());
-                    if (!mGatt.readCharacteristic(chr)) {
-                        Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
-                        mResult = false;
-                        break;
-                    }
-                    mResult = true;
-                    break;
-                case CHR_WRITE:
-                    chr = getCharacteristic(mUuid);
-                    //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
-                    chr.setValue(mValue);
-                    if (!mGatt.writeCharacteristic(chr)) {
-                        Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
-                        mResult = false;
-                        break;
-                    }
-                    mResult = true;
-                    break;
-                case ENABLE_NOTIFICATION:
-                    chr = getCharacteristic(mUuid);
-                    //Log.v(TAG, "Writing descriptor of " + chr.getUuid());
-                    if (chr != null) {
-                        BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
-                        if (cccd != null) {
-                            int properties = chr.getProperties();
-                            byte[] value;
-                            if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
-                                value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
-                            } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
-                                value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
-                            } else {
-                                Log.e(TAG, "Unable to start notifications on input characteristic");
-                                mResult = false;
-                                return;
-                            }
-
-                            mGatt.setCharacteristicNotification(chr, true);
-                            cccd.setValue(value);
-                            if (!mGatt.writeDescriptor(cccd)) {
-                                Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
-                                mResult = false;
-                                return;
-                            }
-                            mResult = true;
-                        }
-                    }
-            }
-        }
-
-        public boolean finish() {
-            return mResult;
-        }
-
-        private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
-            BluetoothGattService valveService = mGatt.getService(steamControllerService);
-            if (valveService == null)
-                return null;
-            return valveService.getCharacteristic(uuid);
-        }
-
-        static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
-            return new GattOperation(gatt, Operation.CHR_READ, uuid);
-        }
-
-        static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
-            return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
-        }
-
-        static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
-            return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
-        }
-    }
-
-    public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
-        mManager = manager;
-        mDevice = device;
-        mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
-        mIsRegistered = false;
-        mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
-        mOperations = new LinkedList<GattOperation>();
-        mHandler = new Handler(Looper.getMainLooper());
-
-        mGatt = connectGatt();
-        // final HIDDeviceBLESteamController finalThis = this;
-        // mHandler.postDelayed(new Runnable() {
-        //     @Override
-        //     public void run() {
-        //         finalThis.checkConnectionForChromebookIssue();
-        //     }
-        // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
-    }
-
-    public String getIdentifier() {
-        return String.format("SteamController.%s", mDevice.getAddress());
-    }
-
-    public BluetoothGatt getGatt() {
-        return mGatt;
-    }
-
-    // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
-    // of TRANSPORT_LE.  Let's force ourselves to connect low energy.
-    private BluetoothGatt connectGatt(boolean managed) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            try {
-                return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
-            } catch (Exception e) {
-                return mDevice.connectGatt(mManager.getContext(), managed, this);
-            }
-        } else {
-            return mDevice.connectGatt(mManager.getContext(), managed, this);
-        }
-    }
-
-    private BluetoothGatt connectGatt() {
-        return connectGatt(false);
-    }
-
-    protected int getConnectionState() {
-
-        Context context = mManager.getContext();
-        if (context == null) {
-            // We are lacking any context to get our Bluetooth information.  We'll just assume disconnected.
-            return BluetoothProfile.STATE_DISCONNECTED;
-        }
-
-        BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
-        if (btManager == null) {
-            // This device doesn't support Bluetooth.  We should never be here, because how did
-            // we instantiate a device to start with?
-            return BluetoothProfile.STATE_DISCONNECTED;
-        }
-
-        return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
-    }
-
-    public void reconnect() {
-
-        if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
-            mGatt.disconnect();
-            mGatt = connectGatt();
-        }
-
-    }
-
-    protected void checkConnectionForChromebookIssue() {
-        if (!mIsChromebook) {
-            // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
-            // over and over.
-            return;
-        }
-
-        int connectionState = getConnectionState();
-
-        switch (connectionState) {
-            case BluetoothProfile.STATE_CONNECTED:
-                if (!mIsConnected) {
-                    // We are in the Bad Chromebook Place.  We can force a disconnect
-                    // to try to recover.
-                    Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback.  Forcing a reconnect.");
-                    mIsReconnecting = true;
-                    mGatt.disconnect();
-                    mGatt = connectGatt(false);
-                    break;
-                }
-                else if (!isRegistered()) {
-                    if (mGatt.getServices().size() > 0) {
-                        Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration.  Trying to recover.");
-                        probeService(this);
-                    }
-                    else {
-                        Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services.  Trying to recover.");
-                        mIsReconnecting = true;
-                        mGatt.disconnect();
-                        mGatt = connectGatt(false);
-                        break;
-                    }
-                }
-                else {
-                    Log.v(TAG, "Chromebook: We are connected, and registered.  Everything's good!");
-                    return;
-                }
-                break;
-
-            case BluetoothProfile.STATE_DISCONNECTED:
-                Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us.  Attempting a disconnect/reconnect, but we may not be able to recover.");
-
-                mIsReconnecting = true;
-                mGatt.disconnect();
-                mGatt = connectGatt(false);
-                break;
-
-            case BluetoothProfile.STATE_CONNECTING:
-                Log.v(TAG, "Chromebook: We're still trying to connect.  Waiting a bit longer.");
-                break;
-        }
-
-        final HIDDeviceBLESteamController finalThis = this;
-        mHandler.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                finalThis.checkConnectionForChromebookIssue();
-            }
-        }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
-    }
-
-    private boolean isRegistered() {
-        return mIsRegistered;
-    }
-
-    private void setRegistered() {
-        mIsRegistered = true;
-    }
-
-    private boolean probeService(HIDDeviceBLESteamController controller) {
-
-        if (isRegistered()) {
-            return true;
-        }
-
-        if (!mIsConnected) {
-            return false;
-        }
-
-        Log.v(TAG, "probeService controller=" + controller);
-
-        for (BluetoothGattService service : mGatt.getServices()) {
-            if (service.getUuid().equals(steamControllerService)) {
-                Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
-
-                for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
-                    if (chr.getUuid().equals(inputCharacteristic)) {
-                        Log.v(TAG, "Found input characteristic");
-                        // Start notifications
-                        BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
-                        if (cccd != null) {
-                            enableNotification(chr.getUuid());
-                        }
-                    }
-                }
-                return true;
-            }
-        }
-
-        if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
-            Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
-            mIsConnected = false;
-            mIsReconnecting = true;
-            mGatt.disconnect();
-            mGatt = connectGatt(false);
-        }
-
-        return false;
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void finishCurrentGattOperation() {
-        GattOperation op = null;
-        synchronized (mOperations) {
-            if (mCurrentOperation != null) {
-                op = mCurrentOperation;
-                mCurrentOperation = null;
-            }
-        }
-        if (op != null) {
-            boolean result = op.finish(); // TODO: Maybe in main thread as well?
-
-            // Our operation failed, let's add it back to the beginning of our queue.
-            if (!result) {
-                mOperations.addFirst(op);
-            }
-        }
-        executeNextGattOperation();
-    }
-
-    private void executeNextGattOperation() {
-        synchronized (mOperations) {
-            if (mCurrentOperation != null)
-                return;
-
-            if (mOperations.isEmpty())
-                return;
-
-            mCurrentOperation = mOperations.removeFirst();
-        }
-
-        // Run in main thread
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                synchronized (mOperations) {
-                    if (mCurrentOperation == null) {
-                        Log.e(TAG, "Current operation null in executor?");
-                        return;
-                    }
-
-                    mCurrentOperation.run();
-                    // now wait for the GATT callback and when it comes, finish this operation
-                }
-            }
-        });
-    }
-
-    private void queueGattOperation(GattOperation op) {
-        synchronized (mOperations) {
-            mOperations.add(op);
-        }
-        executeNextGattOperation();
-    }
-
-    private void enableNotification(UUID chrUuid) {
-        GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
-        queueGattOperation(op);
-    }
-
-    public void writeCharacteristic(UUID uuid, byte[] value) {
-        GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
-        queueGattOperation(op);
-    }
-
-    public void readCharacteristic(UUID uuid) {
-        GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
-        queueGattOperation(op);
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////////////  BluetoothGattCallback overridden methods
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
-        //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
-        mIsReconnecting = false;
-        if (newState == 2) {
-            mIsConnected = true;
-            // Run directly, without GattOperation
-            if (!isRegistered()) {
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        mGatt.discoverServices();
-                    }
-                });
-            }
-        } 
-        else if (newState == 0) {
-            mIsConnected = false;
-        }
-
-        // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
-    }
-
-    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
-        //Log.v(TAG, "onServicesDiscovered status=" + status);
-        if (status == 0) {
-            if (gatt.getServices().size() == 0) {
-                Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
-                mIsReconnecting = true;
-                mIsConnected = false;
-                gatt.disconnect();
-                mGatt = connectGatt(false);
-            }
-            else {
-                probeService(this);
-            }
-        }
-    }
-
-    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
-        //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
-
-        if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
-            mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
-        }
-
-        finishCurrentGattOperation();
-    }
-
-    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
-        //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
-
-        if (characteristic.getUuid().equals(reportCharacteristic)) {
-            // Only register controller with the native side once it has been fully configured
-            if (!isRegistered()) {
-                Log.v(TAG, "Registering Steam Controller with ID: " + getId());
-                mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
-                setRegistered();
-            }
-        }
-
-        finishCurrentGattOperation();
-    }
-
-    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
-    // Enable this for verbose logging of controller input reports
-        //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
-
-        if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
-            mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
-        }
-    }
-
-    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
-        //Log.v(TAG, "onDescriptorRead status=" + status);
-    }
-
-    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
-        BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
-        //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
-
-        if (chr.getUuid().equals(inputCharacteristic)) {
-            boolean hasWrittenInputDescriptor = true;
-            BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
-            if (reportChr != null) {
-                Log.v(TAG, "Writing report characteristic to enter valve mode");
-                reportChr.setValue(enterValveMode);
-                gatt.writeCharacteristic(reportChr);
-            }
-        }
-
-        finishCurrentGattOperation();
-    }
-
-    public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
-        //Log.v(TAG, "onReliableWriteCompleted status=" + status);
-    }
-
-    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
-        //Log.v(TAG, "onReadRemoteRssi status=" + status);
-    }
-
-    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
-        //Log.v(TAG, "onMtuChanged status=" + status);
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////// Public API
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public int getId() {
-        return mDeviceId;
-    }
-
-    @Override
-    public int getVendorId() {
-        // Valve Corporation
-        final int VALVE_USB_VID = 0x28DE;
-        return VALVE_USB_VID;
-    }
-
-    @Override
-    public int getProductId() {
-        // We don't have an easy way to query from the Bluetooth device, but we know what it is
-        final int D0G_BLE2_PID = 0x1106;
-        return D0G_BLE2_PID;
-    }
-
-    @Override
-    public String getSerialNumber() {
-        // This will be read later via feature report by Steam
-        return "12345";
-    }
-
-    @Override
-    public int getVersion() {
-        return 0;
-    }
-
-    @Override
-    public String getManufacturerName() {
-        return "Valve Corporation";
-    }
-
-    @Override
-    public String getProductName() {
-        return "Steam Controller";
-    }
-
-    @Override
-    public UsbDevice getDevice() {
-        return null;
-    }
-
-    @Override
-    public boolean open() {
-        return true;
-    }
-
-    @Override
-    public int sendFeatureReport(byte[] report) {
-        if (!isRegistered()) {
-            Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
-            if (mIsConnected) {
-                probeService(this);
-            }
-            return -1;
-        }
-
-        // We need to skip the first byte, as that doesn't go over the air
-        byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
-        //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
-        writeCharacteristic(reportCharacteristic, actual_report);
-        return report.length;
-    }
-
-    @Override
-    public int sendOutputReport(byte[] report) {
-        if (!isRegistered()) {
-            Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
-            if (mIsConnected) {
-                probeService(this);
-            }
-            return -1;
-        }
-
-        //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
-        writeCharacteristic(reportCharacteristic, report);
-        return report.length;
-    }
-
-    @Override
-    public boolean getFeatureReport(byte[] report) {
-        if (!isRegistered()) {
-            Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
-            if (mIsConnected) {
-                probeService(this);
-            }
-            return false;
-        }
-
-        //Log.v(TAG, "getFeatureReport");
-        readCharacteristic(reportCharacteristic);
-        return true;
-    }
-
-    @Override
-    public void close() {
-    }
-
-    @Override
-    public void setFrozen(boolean frozen) {
-        mFrozen = frozen;
-    }
-
-    @Override
-    public void shutdown() {
-        close();
-
-        BluetoothGatt g = mGatt;
-        if (g != null) {
-            g.disconnect();
-            g.close();
-            mGatt = null;
-        }
-        mManager = null;
-        mIsRegistered = false;
-        mIsConnected = false;
-        mOperations.clear();
-    }
-
-}
-

+ 0 - 679
android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java

@@ -1,679 +0,0 @@
-package org.libsdl.app;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.PendingIntent;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.os.Build;
-import android.util.Log;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.hardware.usb.*;
-import android.os.Handler;
-import android.os.Looper;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
-public class HIDDeviceManager {
-    private static final String TAG = "hidapi";
-    private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
-
-    private static HIDDeviceManager sManager;
-    private static int sManagerRefCount = 0;
-
-    public static HIDDeviceManager acquire(Context context) {
-        if (sManagerRefCount == 0) {
-            sManager = new HIDDeviceManager(context);
-        }
-        ++sManagerRefCount;
-        return sManager;
-    }
-
-    public static void release(HIDDeviceManager manager) {
-        if (manager == sManager) {
-            --sManagerRefCount;
-            if (sManagerRefCount == 0) {
-                sManager.close();
-                sManager = null;
-            }
-        }
-    }
-
-    private Context mContext;
-    private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
-    private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
-    private int mNextDeviceId = 0;
-    private SharedPreferences mSharedPreferences = null;
-    private boolean mIsChromebook = false;
-    private UsbManager mUsbManager;
-    private Handler mHandler;
-    private BluetoothManager mBluetoothManager;
-    private List<BluetoothDevice> mLastBluetoothDevices;
-
-    private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
-                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
-                handleUsbDeviceAttached(usbDevice);
-            } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
-                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
-                handleUsbDeviceDetached(usbDevice);
-            } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
-                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
-                handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
-            }
-        }
-    };
-
-    private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            // Bluetooth device was connected. If it was a Steam Controller, handle it
-            if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
-                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-                Log.d(TAG, "Bluetooth device connected: " + device);
-
-                if (isSteamController(device)) {
-                    connectBluetoothDevice(device);
-                }
-            }
-
-            // Bluetooth device was disconnected, remove from controller manager (if any)
-            if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
-                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-                Log.d(TAG, "Bluetooth device disconnected: " + device);
-
-                disconnectBluetoothDevice(device);
-            }
-        }
-    };
-
-    private HIDDeviceManager(final Context context) {
-        mContext = context;
-
-        HIDDeviceRegisterCallback();
-
-        mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
-        mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
-
-//        if (shouldClear) {
-//            SharedPreferences.Editor spedit = mSharedPreferences.edit();
-//            spedit.clear();
-//            spedit.commit();
-//        }
-//        else
-        {
-            mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
-        }
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    public int getDeviceIDForIdentifier(String identifier) {
-        SharedPreferences.Editor spedit = mSharedPreferences.edit();
-
-        int result = mSharedPreferences.getInt(identifier, 0);
-        if (result == 0) {
-            result = mNextDeviceId++;
-            spedit.putInt("next_device_id", mNextDeviceId);
-        }
-
-        spedit.putInt(identifier, result);
-        spedit.commit();
-        return result;
-    }
-
-    private void initializeUSB() {
-        mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
-        if (mUsbManager == null) {
-            return;
-        }
-
-        /*
-        // Logging
-        for (UsbDevice device : mUsbManager.getDeviceList().values()) {
-            Log.i(TAG,"Path: " + device.getDeviceName());
-            Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
-            Log.i(TAG,"Product: " + device.getProductName());
-            Log.i(TAG,"ID: " + device.getDeviceId());
-            Log.i(TAG,"Class: " + device.getDeviceClass());
-            Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
-            Log.i(TAG,"Vendor ID " + device.getVendorId());
-            Log.i(TAG,"Product ID: " + device.getProductId());
-            Log.i(TAG,"Interface count: " + device.getInterfaceCount());
-            Log.i(TAG,"---------------------------------------");
-
-            // Get interface details
-            for (int index = 0; index < device.getInterfaceCount(); index++) {
-                UsbInterface mUsbInterface = device.getInterface(index);
-                Log.i(TAG,"  *****     *****");
-                Log.i(TAG,"  Interface index: " + index);
-                Log.i(TAG,"  Interface ID: " + mUsbInterface.getId());
-                Log.i(TAG,"  Interface class: " + mUsbInterface.getInterfaceClass());
-                Log.i(TAG,"  Interface subclass: " + mUsbInterface.getInterfaceSubclass());
-                Log.i(TAG,"  Interface protocol: " + mUsbInterface.getInterfaceProtocol());
-                Log.i(TAG,"  Endpoint count: " + mUsbInterface.getEndpointCount());
-
-                // Get endpoint details 
-                for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
-                {
-                    UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
-                    Log.i(TAG,"    ++++   ++++   ++++");
-                    Log.i(TAG,"    Endpoint index: " + epi);
-                    Log.i(TAG,"    Attributes: " + mEndpoint.getAttributes());
-                    Log.i(TAG,"    Direction: " + mEndpoint.getDirection());
-                    Log.i(TAG,"    Number: " + mEndpoint.getEndpointNumber());
-                    Log.i(TAG,"    Interval: " + mEndpoint.getInterval());
-                    Log.i(TAG,"    Packet size: " + mEndpoint.getMaxPacketSize());
-                    Log.i(TAG,"    Type: " + mEndpoint.getType());
-                }
-            }
-        }
-        Log.i(TAG," No more devices connected.");
-        */
-
-        // Register for USB broadcasts and permission completions
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
-        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
-        filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
-        mContext.registerReceiver(mUsbBroadcast, filter);
-
-        for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
-            handleUsbDeviceAttached(usbDevice);
-        }
-    }
-
-    UsbManager getUSBManager() {
-        return mUsbManager;
-    }
-
-    private void shutdownUSB() {
-        try {
-            mContext.unregisterReceiver(mUsbBroadcast);
-        } catch (Exception e) {
-            // We may not have registered, that's okay
-        }
-    }
-
-    private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
-        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
-            return true;
-        }
-        if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
-            return true;
-        }
-        return false;
-    }
-
-    private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
-        final int XB360_IFACE_SUBCLASS = 93;
-        final int XB360_IFACE_PROTOCOL = 1; // Wired
-        final int XB360W_IFACE_PROTOCOL = 129; // Wireless
-        final int[] SUPPORTED_VENDORS = {
-            0x0079, // GPD Win 2
-            0x044f, // Thrustmaster
-            0x045e, // Microsoft
-            0x046d, // Logitech
-            0x056e, // Elecom
-            0x06a3, // Saitek
-            0x0738, // Mad Catz
-            0x07ff, // Mad Catz
-            0x0e6f, // PDP
-            0x0f0d, // Hori
-            0x1038, // SteelSeries
-            0x11c9, // Nacon
-            0x12ab, // Unknown
-            0x1430, // RedOctane
-            0x146b, // BigBen
-            0x1532, // Razer Sabertooth
-            0x15e4, // Numark
-            0x162e, // Joytech
-            0x1689, // Razer Onza
-            0x1949, // Lab126, Inc.
-            0x1bad, // Harmonix
-            0x20d6, // PowerA
-            0x24c6, // PowerA
-            0x2c22, // Qanba
-        };
-
-        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
-            usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
-            (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
-             usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
-            int vendor_id = usbDevice.getVendorId();
-            for (int supportedVid : SUPPORTED_VENDORS) {
-                if (vendor_id == supportedVid) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
-        final int XB1_IFACE_SUBCLASS = 71;
-        final int XB1_IFACE_PROTOCOL = 208;
-        final int[] SUPPORTED_VENDORS = {
-            0x045e, // Microsoft
-            0x0738, // Mad Catz
-            0x0e6f, // PDP
-            0x0f0d, // Hori
-            0x1532, // Razer Wildcat
-            0x20d6, // PowerA
-            0x24c6, // PowerA
-            0x2dc8, /* 8BitDo */
-            0x2e24, // Hyperkin
-        };
-
-        if (usbInterface.getId() == 0 &&
-            usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
-            usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
-            usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
-            int vendor_id = usbDevice.getVendorId();
-            for (int supportedVid : SUPPORTED_VENDORS) {
-                if (vendor_id == supportedVid) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private void handleUsbDeviceAttached(UsbDevice usbDevice) {
-        connectHIDDeviceUSB(usbDevice);
-    }
-
-    private void handleUsbDeviceDetached(UsbDevice usbDevice) {
-        List<Integer> devices = new ArrayList<Integer>();
-        for (HIDDevice device : mDevicesById.values()) {
-            if (usbDevice.equals(device.getDevice())) {
-                devices.add(device.getId());
-            }
-        }
-        for (int id : devices) {
-            HIDDevice device = mDevicesById.get(id);
-            mDevicesById.remove(id);
-            device.shutdown();
-            HIDDeviceDisconnected(id);
-        }
-    }
-
-    private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
-        for (HIDDevice device : mDevicesById.values()) {
-            if (usbDevice.equals(device.getDevice())) {
-                boolean opened = false;
-                if (permission_granted) {
-                    opened = device.open();
-                }
-                HIDDeviceOpenResult(device.getId(), opened);
-            }
-        }
-    }
-
-    private void connectHIDDeviceUSB(UsbDevice usbDevice) {
-        synchronized (this) {
-            int interface_mask = 0;
-            for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
-                UsbInterface usbInterface = usbDevice.getInterface(interface_index);
-                if (isHIDDeviceInterface(usbDevice, usbInterface)) {
-                    // Check to see if we've already added this interface
-                    // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
-                    int interface_id = usbInterface.getId();
-                    if ((interface_mask & (1 << interface_id)) != 0) {
-                        continue;
-                    }
-                    interface_mask |= (1 << interface_id);
-
-                    HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
-                    int id = device.getId();
-                    mDevicesById.put(id, device);
-                    HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
-                }
-            }
-        }
-    }
-
-    private void initializeBluetooth() {
-        Log.d(TAG, "Initializing Bluetooth");
-
-        if (Build.VERSION.SDK_INT <= 30 &&
-            mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
-            Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
-            return;
-        }
-
-        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) {
-            Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
-            return;
-        }
-
-        // Find bonded bluetooth controllers and create SteamControllers for them
-        mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
-        if (mBluetoothManager == null) {
-            // This device doesn't support Bluetooth.
-            return;
-        }
-
-        BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
-        if (btAdapter == null) {
-            // This device has Bluetooth support in the codebase, but has no available adapters.
-            return;
-        }
-
-        // Get our bonded devices.
-        for (BluetoothDevice device : btAdapter.getBondedDevices()) {
-
-            Log.d(TAG, "Bluetooth device available: " + device);
-            if (isSteamController(device)) {
-                connectBluetoothDevice(device);
-            }
-
-        }
-
-        // NOTE: These don't work on Chromebooks, to my undying dismay.
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
-        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
-        mContext.registerReceiver(mBluetoothBroadcast, filter);
-
-        if (mIsChromebook) {
-            mHandler = new Handler(Looper.getMainLooper());
-            mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
-
-            // final HIDDeviceManager finalThis = this;
-            // mHandler.postDelayed(new Runnable() {
-            //     @Override
-            //     public void run() {
-            //         finalThis.chromebookConnectionHandler();
-            //     }
-            // }, 5000);
-        }
-    }
-
-    private void shutdownBluetooth() {
-        try {
-            mContext.unregisterReceiver(mBluetoothBroadcast);
-        } catch (Exception e) {
-            // We may not have registered, that's okay
-        }
-    }
-
-    // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
-    // This function provides a sort of dummy version of that, watching for changes in the
-    // connected devices and attempting to add controllers as things change.
-    public void chromebookConnectionHandler() {
-        if (!mIsChromebook) {
-            return;
-        }
-
-        ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
-        ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
-
-        List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
-
-        for (BluetoothDevice bluetoothDevice : currentConnected) {
-            if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
-                connected.add(bluetoothDevice);
-            }
-        }
-        for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
-            if (!currentConnected.contains(bluetoothDevice)) {
-                disconnected.add(bluetoothDevice);
-            }
-        }
-
-        mLastBluetoothDevices = currentConnected;
-
-        for (BluetoothDevice bluetoothDevice : disconnected) {
-            disconnectBluetoothDevice(bluetoothDevice);
-        }
-        for (BluetoothDevice bluetoothDevice : connected) {
-            connectBluetoothDevice(bluetoothDevice);
-        }
-
-        final HIDDeviceManager finalThis = this;
-        mHandler.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                finalThis.chromebookConnectionHandler();
-            }
-        }, 10000);
-    }
-
-    public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
-        Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
-        synchronized (this) {
-            if (mBluetoothDevices.containsKey(bluetoothDevice)) {
-                Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
-
-                HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
-                device.reconnect();
-
-                return false;
-            }
-            HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
-            int id = device.getId();
-            mBluetoothDevices.put(bluetoothDevice, device);
-            mDevicesById.put(id, device);
-
-            // The Steam Controller will mark itself connected once initialization is complete
-        }
-        return true;
-    }
-
-    public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
-        synchronized (this) {
-            HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
-            if (device == null)
-                return;
-
-            int id = device.getId();
-            mBluetoothDevices.remove(bluetoothDevice);
-            mDevicesById.remove(id);
-            device.shutdown();
-            HIDDeviceDisconnected(id);
-        }
-    }
-
-    public boolean isSteamController(BluetoothDevice bluetoothDevice) {
-        // Sanity check.  If you pass in a null device, by definition it is never a Steam Controller.
-        if (bluetoothDevice == null) {
-            return false;
-        }
-
-        // If the device has no local name, we really don't want to try an equality check against it.
-        if (bluetoothDevice.getName() == null) {
-            return false;
-        }
-
-        return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
-    }
-
-    private void close() {
-        shutdownUSB();
-        shutdownBluetooth();
-        synchronized (this) {
-            for (HIDDevice device : mDevicesById.values()) {
-                device.shutdown();
-            }
-            mDevicesById.clear();
-            mBluetoothDevices.clear();
-            HIDDeviceReleaseCallback();
-        }
-    }
-
-    public void setFrozen(boolean frozen) {
-        synchronized (this) {
-            for (HIDDevice device : mDevicesById.values()) {
-                device.setFrozen(frozen);
-            }
-        }        
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private HIDDevice getDevice(int id) {
-        synchronized (this) {
-            HIDDevice result = mDevicesById.get(id);
-            if (result == null) {
-                Log.v(TAG, "No device for id: " + id);
-                Log.v(TAG, "Available devices: " + mDevicesById.keySet());
-            }
-            return result;
-        }
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    ////////// JNI interface functions
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean initialize(boolean usb, boolean bluetooth) {
-        Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
-
-        if (usb) {
-            initializeUSB();
-        }
-        if (bluetooth) {
-            initializeBluetooth();
-        }
-        return true;
-    }
-
-    public boolean openDevice(int deviceID) {
-        Log.v(TAG, "openDevice deviceID=" + deviceID);
-        HIDDevice device = getDevice(deviceID);
-        if (device == null) {
-            HIDDeviceDisconnected(deviceID);
-            return false;
-        }
-
-        // Look to see if this is a USB device and we have permission to access it
-        UsbDevice usbDevice = device.getDevice();
-        if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
-            HIDDeviceOpenPending(deviceID);
-            try {
-                final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
-                int flags;
-                if (Build.VERSION.SDK_INT >= 31) {
-                    flags = FLAG_MUTABLE;
-                } else {
-                    flags = 0;
-                }
-                mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
-            } catch (Exception e) {
-                Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
-                HIDDeviceOpenResult(deviceID, false);
-            }
-            return false;
-        }
-
-        try {
-            return device.open();
-        } catch (Exception e) {
-            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
-        }
-        return false;
-    }
-
-    public int sendOutputReport(int deviceID, byte[] report) {
-        try {
-            //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
-            HIDDevice device;
-            device = getDevice(deviceID);
-            if (device == null) {
-                HIDDeviceDisconnected(deviceID);
-                return -1;
-            }
-
-            return device.sendOutputReport(report);
-        } catch (Exception e) {
-            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
-        }
-        return -1;
-    }
-
-    public int sendFeatureReport(int deviceID, byte[] report) {
-        try {
-            //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
-            HIDDevice device;
-            device = getDevice(deviceID);
-            if (device == null) {
-                HIDDeviceDisconnected(deviceID);
-                return -1;
-            }
-
-            return device.sendFeatureReport(report);
-        } catch (Exception e) {
-            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
-        }
-        return -1;
-    }
-
-    public boolean getFeatureReport(int deviceID, byte[] report) {
-        try {
-            //Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
-            HIDDevice device;
-            device = getDevice(deviceID);
-            if (device == null) {
-                HIDDeviceDisconnected(deviceID);
-                return false;
-            }
-
-            return device.getFeatureReport(report);
-        } catch (Exception e) {
-            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
-        }
-        return false;
-    }
-
-    public void closeDevice(int deviceID) {
-        try {
-            Log.v(TAG, "closeDevice deviceID=" + deviceID);
-            HIDDevice device;
-            device = getDevice(deviceID);
-            if (device == null) {
-                HIDDeviceDisconnected(deviceID);
-                return;
-            }
-
-            device.close();
-        } catch (Exception e) {
-            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
-        }
-    }
-
-
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-    /////////////// Native methods
-    //////////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private native void HIDDeviceRegisterCallback();
-    private native void HIDDeviceReleaseCallback();
-
-    native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
-    native void HIDDeviceOpenPending(int deviceID);
-    native void HIDDeviceOpenResult(int deviceID, boolean opened);
-    native void HIDDeviceDisconnected(int deviceID);
-
-    native void HIDDeviceInputReport(int deviceID, byte[] report);
-    native void HIDDeviceFeatureReport(int deviceID, byte[] report);
-}

+ 0 - 309
android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java

@@ -1,309 +0,0 @@
-package org.libsdl.app;
-
-import android.hardware.usb.*;
-import android.os.Build;
-import android.util.Log;
-import java.util.Arrays;
-
-class HIDDeviceUSB implements HIDDevice {
-
-    private static final String TAG = "hidapi";
-
-    protected HIDDeviceManager mManager;
-    protected UsbDevice mDevice;
-    protected int mInterfaceIndex;
-    protected int mInterface;
-    protected int mDeviceId;
-    protected UsbDeviceConnection mConnection;
-    protected UsbEndpoint mInputEndpoint;
-    protected UsbEndpoint mOutputEndpoint;
-    protected InputThread mInputThread;
-    protected boolean mRunning;
-    protected boolean mFrozen;
-
-    public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
-        mManager = manager;
-        mDevice = usbDevice;
-        mInterfaceIndex = interface_index;
-        mInterface = mDevice.getInterface(mInterfaceIndex).getId();
-        mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
-        mRunning = false;
-    }
-
-    public String getIdentifier() {
-        return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
-    }
-
-    @Override
-    public int getId() {
-        return mDeviceId;
-    }
-
-    @Override
-    public int getVendorId() {
-        return mDevice.getVendorId();
-    }
-
-    @Override
-    public int getProductId() {
-        return mDevice.getProductId();
-    }
-
-    @Override
-    public String getSerialNumber() {
-        String result = null;
-        if (Build.VERSION.SDK_INT >= 21) {
-            try {
-                result = mDevice.getSerialNumber();
-            }
-            catch (SecurityException exception) {
-                //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
-            }
-        }
-        if (result == null) {
-            result = "";
-        }
-        return result;
-    }
-
-    @Override
-    public int getVersion() {
-        return 0;
-    }
-
-    @Override
-    public String getManufacturerName() {
-        String result = null;
-        if (Build.VERSION.SDK_INT >= 21) {
-            result = mDevice.getManufacturerName();
-        }
-        if (result == null) {
-            result = String.format("%x", getVendorId());
-        }
-        return result;
-    }
-
-    @Override
-    public String getProductName() {
-        String result = null;
-        if (Build.VERSION.SDK_INT >= 21) {
-            result = mDevice.getProductName();
-        }
-        if (result == null) {
-            result = String.format("%x", getProductId());
-        }
-        return result;
-    }
-
-    @Override
-    public UsbDevice getDevice() {
-        return mDevice;
-    }
-
-    public String getDeviceName() {
-        return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
-    }
-
-    @Override
-    public boolean open() {
-        mConnection = mManager.getUSBManager().openDevice(mDevice);
-        if (mConnection == null) {
-            Log.w(TAG, "Unable to open USB device " + getDeviceName());
-            return false;
-        }
-
-        // Force claim our interface
-        UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
-        if (!mConnection.claimInterface(iface, true)) {
-            Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
-            close();
-            return false;
-        }
-
-        // Find the endpoints
-        for (int j = 0; j < iface.getEndpointCount(); j++) {
-            UsbEndpoint endpt = iface.getEndpoint(j);
-            switch (endpt.getDirection()) {
-            case UsbConstants.USB_DIR_IN:
-                if (mInputEndpoint == null) {
-                    mInputEndpoint = endpt;
-                }
-                break;
-            case UsbConstants.USB_DIR_OUT:
-                if (mOutputEndpoint == null) {
-                    mOutputEndpoint = endpt;
-                }
-                break;
-            }
-        }
-
-        // Make sure the required endpoints were present
-        if (mInputEndpoint == null || mOutputEndpoint == null) {
-            Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
-            close();
-            return false;
-        }
-
-        // Start listening for input
-        mRunning = true;
-        mInputThread = new InputThread();
-        mInputThread.start();
-
-        return true;
-    }
-
-    @Override
-    public int sendFeatureReport(byte[] report) {
-        int res = -1;
-        int offset = 0;
-        int length = report.length;
-        boolean skipped_report_id = false;
-        byte report_number = report[0];
-
-        if (report_number == 0x0) {
-            ++offset;
-            --length;
-            skipped_report_id = true;
-        }
-
-        res = mConnection.controlTransfer(
-            UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
-            0x09/*HID set_report*/,
-            (3/*HID feature*/ << 8) | report_number,
-            mInterface,
-            report, offset, length,
-            1000/*timeout millis*/);
-
-        if (res < 0) {
-            Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
-            return -1;
-        }
-
-        if (skipped_report_id) {
-            ++length;
-        }
-        return length;
-    }
-
-    @Override
-    public int sendOutputReport(byte[] report) {
-        int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
-        if (r != report.length) {
-            Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
-        }
-        return r;
-    }
-
-    @Override
-    public boolean getFeatureReport(byte[] report) {
-        int res = -1;
-        int offset = 0;
-        int length = report.length;
-        boolean skipped_report_id = false;
-        byte report_number = report[0];
-
-        if (report_number == 0x0) {
-            /* Offset the return buffer by 1, so that the report ID
-               will remain in byte 0. */
-            ++offset;
-            --length;
-            skipped_report_id = true;
-        }
-
-        res = mConnection.controlTransfer(
-            UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
-            0x01/*HID get_report*/,
-            (3/*HID feature*/ << 8) | report_number,
-            mInterface,
-            report, offset, length,
-            1000/*timeout millis*/);
-
-        if (res < 0) {
-            Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
-            return false;
-        }
-
-        if (skipped_report_id) {
-            ++res;
-            ++length;
-        }
-
-        byte[] data;
-        if (res == length) {
-            data = report;
-        } else {
-            data = Arrays.copyOfRange(report, 0, res);
-        }
-        mManager.HIDDeviceFeatureReport(mDeviceId, data);
-
-        return true;
-    }
-
-    @Override
-    public void close() {
-        mRunning = false;
-        if (mInputThread != null) {
-            while (mInputThread.isAlive()) {
-                mInputThread.interrupt();
-                try {
-                    mInputThread.join();
-                } catch (InterruptedException e) {
-                    // Keep trying until we're done
-                }
-            }
-            mInputThread = null;
-        }
-        if (mConnection != null) {
-            UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
-            mConnection.releaseInterface(iface);
-            mConnection.close();
-            mConnection = null;
-        }
-    }
-
-    @Override
-    public void shutdown() {
-        close();
-        mManager = null;
-    }
-
-    @Override
-    public void setFrozen(boolean frozen) {
-        mFrozen = frozen;
-    }
-
-    protected class InputThread extends Thread {
-        @Override
-        public void run() {
-            int packetSize = mInputEndpoint.getMaxPacketSize();
-            byte[] packet = new byte[packetSize];
-            while (mRunning) {
-                int r;
-                try
-                {
-                    r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
-                }
-                catch (Exception e)
-                {
-                    Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
-                    break;
-                }
-                if (r < 0) {
-                    // Could be a timeout or an I/O error
-                }
-                if (r > 0) {
-                    byte[] data;
-                    if (r == packetSize) {
-                        data = packet;
-                    } else {
-                        data = Arrays.copyOfRange(packet, 0, r);
-                    }
-
-                    if (!mFrozen) {
-                        mManager.HIDDeviceInputReport(mDeviceId, data);
-                    }
-                }
-            }
-        }
-    }
-}

+ 0 - 85
android/vcmi-app/src/main/java/org/libsdl/app/SDL.java

@@ -1,85 +0,0 @@
-package org.libsdl.app;
-
-import android.content.Context;
-
-import java.lang.Class;
-import java.lang.reflect.Method;
-
-/**
-    SDL library initialization
-*/
-public class SDL {
-
-    // This function should be called first and sets up the native code
-    // so it can call into the Java classes
-    public static void setupJNI() {
-        SDLActivity.nativeSetupJNI();
-        SDLAudioManager.nativeSetupJNI();
-        SDLControllerManager.nativeSetupJNI();
-    }
-
-    // This function should be called each time the activity is started
-    public static void initialize() {
-        setContext(null);
-
-        SDLActivity.initialize();
-        SDLAudioManager.initialize();
-        SDLControllerManager.initialize();
-    }
-
-    // This function stores the current activity (SDL or not)
-    public static void setContext(Context context) {
-        mContext = context;
-    }
-
-    public static Context getContext() {
-        return mContext;
-    }
-
-    public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
-
-        if (libraryName == null) {
-            throw new NullPointerException("No library name provided.");
-        }
-
-        try {
-            // Let's see if we have ReLinker available in the project.  This is necessary for 
-            // some projects that have huge numbers of local libraries bundled, and thus may 
-            // trip a bug in Android's native library loader which ReLinker works around.  (If
-            // loadLibrary works properly, ReLinker will simply use the normal Android method
-            // internally.)
-            //
-            // To use ReLinker, just add it as a dependency.  For more information, see 
-            // https://github.com/KeepSafe/ReLinker for ReLinker's repository.
-            //
-            Class<?> relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
-            Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
-            Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context");
-            Class<?> stringClass = mContext.getClassLoader().loadClass("java.lang.String");
-
-            // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if 
-            // they've changed during updates.
-            Method forceMethod = relinkClass.getDeclaredMethod("force");
-            Object relinkInstance = forceMethod.invoke(null);
-            Class<?> relinkInstanceClass = relinkInstance.getClass();
-
-            // Actually load the library!
-            Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
-            loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
-        }
-        catch (final Throwable e) {
-            // Fall back
-            try {
-                System.loadLibrary(libraryName);
-            }
-            catch (final UnsatisfiedLinkError ule) {
-                throw ule;
-            }
-            catch (final SecurityException se) {
-                throw se;
-            }
-        }
-    }
-
-    protected static Context mContext;
-}

+ 0 - 2102
android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java

@@ -1,2102 +0,0 @@
-package org.libsdl.app;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.UiModeManager;
-import android.content.ClipboardManager;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.hardware.Sensor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.Selection;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Display;
-import android.view.Gravity;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.PointerIcon;
-import android.view.Surface;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.Hashtable;
-import java.util.Locale;
-
-
-/**
-    SDL Activity
-*/
-public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
-    private static final String TAG = "SDL";
-    private static final int SDL_MAJOR_VERSION = 2;
-    private static final int SDL_MINOR_VERSION = 26;
-    private static final int SDL_MICRO_VERSION = 5;
-/*
-    // Display InputType.SOURCE/CLASS of events and devices
-    //
-    // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]");
-    // SDLActivity.debugSource(event.getSource(), "event");
-    public static void debugSource(int sources, String prefix) {
-        int s = sources;
-        int s_copy = sources;
-        String cls = "";
-        String src = "";
-        int tst = 0;
-        int FLAG_TAINTED = 0x80000000;
-
-        if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0)     cls += " BUTTON";
-        if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)   cls += " JOYSTICK";
-        if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0)    cls += " POINTER";
-        if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0)   cls += " POSITION";
-        if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0)  cls += " TRACKBALL";
-
-
-        int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits
-        s2 &= ~(  InputDevice.SOURCE_CLASS_BUTTON
-                | InputDevice.SOURCE_CLASS_JOYSTICK
-                | InputDevice.SOURCE_CLASS_POINTER
-                | InputDevice.SOURCE_CLASS_POSITION
-                | InputDevice.SOURCE_CLASS_TRACKBALL);
-
-        if (s2 != 0) cls += "Some_Unknown";
-
-        s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class;
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            tst = InputDevice.SOURCE_BLUETOOTH_STYLUS;
-            if ((s & tst) == tst) src += " BLUETOOTH_STYLUS";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_DPAD;
-        if ((s & tst) == tst) src += " DPAD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_GAMEPAD;
-        if ((s & tst) == tst) src += " GAMEPAD";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            tst = InputDevice.SOURCE_HDMI;
-            if ((s & tst) == tst) src += " HDMI";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_JOYSTICK;
-        if ((s & tst) == tst) src += " JOYSTICK";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_KEYBOARD;
-        if ((s & tst) == tst) src += " KEYBOARD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_MOUSE;
-        if ((s & tst) == tst) src += " MOUSE";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= 26) {
-            tst = InputDevice.SOURCE_MOUSE_RELATIVE;
-            if ((s & tst) == tst) src += " MOUSE_RELATIVE";
-            s2 &= ~tst;
-
-            tst = InputDevice.SOURCE_ROTARY_ENCODER;
-            if ((s & tst) == tst) src += " ROTARY_ENCODER";
-            s2 &= ~tst;
-        }
-        tst = InputDevice.SOURCE_STYLUS;
-        if ((s & tst) == tst) src += " STYLUS";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_TOUCHPAD;
-        if ((s & tst) == tst) src += " TOUCHPAD";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_TOUCHSCREEN;
-        if ((s & tst) == tst) src += " TOUCHSCREEN";
-        s2 &= ~tst;
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-            tst = InputDevice.SOURCE_TOUCH_NAVIGATION;
-            if ((s & tst) == tst) src += " TOUCH_NAVIGATION";
-            s2 &= ~tst;
-        }
-
-        tst = InputDevice.SOURCE_TRACKBALL;
-        if ((s & tst) == tst) src += " TRACKBALL";
-        s2 &= ~tst;
-
-        tst = InputDevice.SOURCE_ANY;
-        if ((s & tst) == tst) src += " ANY";
-        s2 &= ~tst;
-
-        if (s == FLAG_TAINTED) src += " FLAG_TAINTED";
-        s2 &= ~FLAG_TAINTED;
-
-        if (s2 != 0) src += " Some_Unknown";
-
-        Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src);
-    }
-*/
-
-    public static boolean mIsResumedCalled, mHasFocus;
-    public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24);
-
-    // Cursor types
-    // private static final int SDL_SYSTEM_CURSOR_NONE = -1;
-    private static final int SDL_SYSTEM_CURSOR_ARROW = 0;
-    private static final int SDL_SYSTEM_CURSOR_IBEAM = 1;
-    private static final int SDL_SYSTEM_CURSOR_WAIT = 2;
-    private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3;
-    private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4;
-    private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5;
-    private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6;
-    private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7;
-    private static final int SDL_SYSTEM_CURSOR_SIZENS = 8;
-    private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9;
-    private static final int SDL_SYSTEM_CURSOR_NO = 10;
-    private static final int SDL_SYSTEM_CURSOR_HAND = 11;
-
-    protected static final int SDL_ORIENTATION_UNKNOWN = 0;
-    protected static final int SDL_ORIENTATION_LANDSCAPE = 1;
-    protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2;
-    protected static final int SDL_ORIENTATION_PORTRAIT = 3;
-    protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4;
-
-    protected static int mCurrentOrientation;
-    protected static Locale mCurrentLocale;
-
-    // Handle the state of the native layer
-    public enum NativeState {
-           INIT, RESUMED, PAUSED
-    }
-
-    public static NativeState mNextNativeState;
-    public static NativeState mCurrentNativeState;
-
-    /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
-    public static boolean mBrokenLibraries = true;
-
-    // Main components
-    protected static SDLActivity mSingleton;
-    protected static SDLSurface mSurface;
-    protected static DummyEdit mTextEdit;
-    protected static boolean mScreenKeyboardShown;
-    protected static ViewGroup mLayout;
-    protected static SDLClipboardHandler mClipboardHandler;
-    protected static Hashtable<Integer, PointerIcon> mCursors;
-    protected static int mLastCursorID;
-    protected static SDLGenericMotionListener_API12 mMotionListener;
-    protected static HIDDeviceManager mHIDDeviceManager;
-
-    // This is what SDL runs in. It invokes SDL_main(), eventually
-    protected static Thread mSDLThread;
-
-    protected static SDLGenericMotionListener_API12 getMotionListener() {
-        if (mMotionListener == null) {
-            if (Build.VERSION.SDK_INT >= 26) {
-                mMotionListener = new SDLGenericMotionListener_API26();
-            } else if (Build.VERSION.SDK_INT >= 24) {
-                mMotionListener = new SDLGenericMotionListener_API24();
-            } else {
-                mMotionListener = new SDLGenericMotionListener_API12();
-            }
-        }
-
-        return mMotionListener;
-    }
-
-    /**
-     * This method returns the name of the shared object with the application entry point
-     * It can be overridden by derived classes.
-     */
-    protected String getMainSharedObject() {
-        String library;
-        String[] libraries = SDLActivity.mSingleton.getLibraries();
-        if (libraries.length > 0) {
-            library = "lib" + libraries[libraries.length - 1] + ".so";
-        } else {
-            library = "libmain.so";
-        }
-        return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
-    }
-
-    /**
-     * This method returns the name of the application entry point
-     * It can be overridden by derived classes.
-     */
-    protected String getMainFunction() {
-        return "SDL_main";
-    }
-
-    /**
-     * This method is called by SDL before loading the native shared libraries.
-     * It can be overridden to provide names of shared libraries to be loaded.
-     * The default implementation returns the defaults. It never returns null.
-     * An array returned by a new implementation must at least contain "SDL2".
-     * Also keep in mind that the order the libraries are loaded may matter.
-     * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
-     */
-    protected String[] getLibraries() {
-        return new String[] {
-            "SDL2",
-            // "SDL2_image",
-            // "SDL2_mixer",
-            // "SDL2_net",
-            // "SDL2_ttf",
-            "main"
-        };
-    }
-
-    // Load the .so
-    public void loadLibraries() {
-       for (String lib : getLibraries()) {
-          SDL.loadLibrary(lib);
-       }
-    }
-
-    /**
-     * This method is called by SDL before starting the native application thread.
-     * It can be overridden to provide the arguments after the application name.
-     * The default implementation returns an empty array. It never returns null.
-     * @return arguments for the native application.
-     */
-    protected String[] getArguments() {
-        return new String[0];
-    }
-
-    public static void initialize() {
-        // The static nature of the singleton and Android quirkyness force us to initialize everything here
-        // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
-        mSingleton = null;
-        mSurface = null;
-        mTextEdit = null;
-        mLayout = null;
-        mClipboardHandler = null;
-        mCursors = new Hashtable<Integer, PointerIcon>();
-        mLastCursorID = 0;
-        mSDLThread = null;
-        mIsResumedCalled = false;
-        mHasFocus = true;
-        mNextNativeState = NativeState.INIT;
-        mCurrentNativeState = NativeState.INIT;
-    }
-    
-    protected SDLSurface createSDLSurface(Context context) {
-        return new SDLSurface(context);
-    }
-
-    // Setup
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        Log.v(TAG, "Device: " + Build.DEVICE);
-        Log.v(TAG, "Model: " + Build.MODEL);
-        Log.v(TAG, "onCreate()");
-        super.onCreate(savedInstanceState);
-
-        try {
-            Thread.currentThread().setName("SDLActivity");
-        } catch (Exception e) {
-            Log.v(TAG, "modify thread properties failed " + e.toString());
-        }
-
-        // Load shared libraries
-        String errorMsgBrokenLib = "";
-        try {
-            loadLibraries();
-            mBrokenLibraries = false; /* success */
-        } catch(UnsatisfiedLinkError e) {
-            System.err.println(e.getMessage());
-            mBrokenLibraries = true;
-            errorMsgBrokenLib = e.getMessage();
-        } catch(Exception e) {
-            System.err.println(e.getMessage());
-            mBrokenLibraries = true;
-            errorMsgBrokenLib = e.getMessage();
-        }
-
-        if (!mBrokenLibraries) {
-            String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." +
-                                      String.valueOf(SDL_MINOR_VERSION) + "." +
-                                      String.valueOf(SDL_MICRO_VERSION);
-            String version = nativeGetVersion();
-            if (!version.equals(expected_version)) {
-                mBrokenLibraries = true;
-                errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")";
-            }
-        }
-
-        if (mBrokenLibraries) {
-            mSingleton = this;
-            AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
-            dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
-                  + System.getProperty("line.separator")
-                  + System.getProperty("line.separator")
-                  + "Error: " + errorMsgBrokenLib);
-            dlgAlert.setTitle("SDL Error");
-            dlgAlert.setPositiveButton("Exit",
-                new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog,int id) {
-                        // if this button is clicked, close current activity
-                        SDLActivity.mSingleton.finish();
-                    }
-                });
-           dlgAlert.setCancelable(false);
-           dlgAlert.create().show();
-
-           return;
-        }
-
-        // Set up JNI
-        SDL.setupJNI();
-
-        // Initialize state
-        SDL.initialize();
-
-        // So we can call stuff from static callbacks
-        mSingleton = this;
-        SDL.setContext(this);
-
-        mClipboardHandler = new SDLClipboardHandler();
-
-        mHIDDeviceManager = HIDDeviceManager.acquire(this);
-
-        // Set up the surface
-        mSurface = createSDLSurface(getApplication());
-
-        mLayout = new RelativeLayout(this);
-        mLayout.addView(mSurface);
-
-        // Get our current screen orientation and pass it down.
-        mCurrentOrientation = SDLActivity.getCurrentOrientation();
-        // Only record current orientation
-        SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
-
-        try {
-            if (Build.VERSION.SDK_INT < 24) {
-                mCurrentLocale = getContext().getResources().getConfiguration().locale;
-            } else {
-                mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0);
-            }
-        } catch(Exception ignored) {
-        }
-
-        setContentView(mLayout);
-
-        setWindowStyle(false);
-
-        getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
-
-        // Get filename from "Open with" of another application
-        Intent intent = getIntent();
-        if (intent != null && intent.getData() != null) {
-            String filename = intent.getData().getPath();
-            if (filename != null) {
-                Log.v(TAG, "Got filename: " + filename);
-                SDLActivity.onNativeDropFile(filename);
-            }
-        }
-    }
-
-    protected void pauseNativeThread() {
-        mNextNativeState = NativeState.PAUSED;
-        mIsResumedCalled = false;
-
-        if (SDLActivity.mBrokenLibraries) {
-            return;
-        }
-
-        SDLActivity.handleNativeState();
-    }
-
-    protected void resumeNativeThread() {
-        mNextNativeState = NativeState.RESUMED;
-        mIsResumedCalled = true;
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        SDLActivity.handleNativeState();
-    }
-
-    // Events
-    @Override
-    protected void onPause() {
-        Log.v(TAG, "onPause()");
-        super.onPause();
-
-        if (mHIDDeviceManager != null) {
-            mHIDDeviceManager.setFrozen(true);
-        }
-        if (!mHasMultiWindow) {
-            pauseNativeThread();
-        }
-    }
-
-    @Override
-    protected void onResume() {
-        Log.v(TAG, "onResume()");
-        super.onResume();
-
-        if (mHIDDeviceManager != null) {
-            mHIDDeviceManager.setFrozen(false);
-        }
-        if (!mHasMultiWindow) {
-            resumeNativeThread();
-        }
-    }
-
-    @Override
-    protected void onStop() {
-        Log.v(TAG, "onStop()");
-        super.onStop();
-        if (mHasMultiWindow) {
-            pauseNativeThread();
-        }
-    }
-
-    @Override
-    protected void onStart() {
-        Log.v(TAG, "onStart()");
-        super.onStart();
-        if (mHasMultiWindow) {
-            resumeNativeThread();
-        }
-    }
-
-    public static int getCurrentOrientation() {
-        int result = SDL_ORIENTATION_UNKNOWN;
-
-        Activity activity = (Activity)getContext();
-        if (activity == null) {
-            return result;
-        }
-        Display display = activity.getWindowManager().getDefaultDisplay();
-
-        switch (display.getRotation()) {
-            case Surface.ROTATION_0:
-                result = SDL_ORIENTATION_PORTRAIT;
-                break;
-
-            case Surface.ROTATION_90:
-                result = SDL_ORIENTATION_LANDSCAPE;
-                break;
-
-            case Surface.ROTATION_180:
-                result = SDL_ORIENTATION_PORTRAIT_FLIPPED;
-                break;
-
-            case Surface.ROTATION_270:
-                result = SDL_ORIENTATION_LANDSCAPE_FLIPPED;
-                break;
-        }
-
-        return result;
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        super.onWindowFocusChanged(hasFocus);
-        Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        mHasFocus = hasFocus;
-        if (hasFocus) {
-           mNextNativeState = NativeState.RESUMED;
-           SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
-
-           SDLActivity.handleNativeState();
-           nativeFocusChanged(true);
-
-        } else {
-           nativeFocusChanged(false);
-           if (!mHasMultiWindow) {
-               mNextNativeState = NativeState.PAUSED;
-               SDLActivity.handleNativeState();
-           }
-        }
-    }
-
-    @Override
-    public void onLowMemory() {
-        Log.v(TAG, "onLowMemory()");
-        super.onLowMemory();
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        SDLActivity.nativeLowMemory();
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        Log.v(TAG, "onConfigurationChanged()");
-        super.onConfigurationChanged(newConfig);
-
-        if (SDLActivity.mBrokenLibraries) {
-           return;
-        }
-
-        if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) {
-            mCurrentLocale = newConfig.locale;
-            SDLActivity.onNativeLocaleChanged();
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        Log.v(TAG, "onDestroy()");
-
-        if (mHIDDeviceManager != null) {
-            HIDDeviceManager.release(mHIDDeviceManager);
-            mHIDDeviceManager = null;
-        }
-
-        if (SDLActivity.mBrokenLibraries) {
-           super.onDestroy();
-           return;
-        }
-
-        if (SDLActivity.mSDLThread != null) {
-
-            // Send Quit event to "SDLThread" thread
-            SDLActivity.nativeSendQuit();
-
-            // Wait for "SDLThread" thread to end
-            try {
-                SDLActivity.mSDLThread.join();
-            } catch(Exception e) {
-                Log.v(TAG, "Problem stopping SDLThread: " + e);
-            }
-        }
-
-        SDLActivity.nativeQuit();
-
-        super.onDestroy();
-    }
-
-    @Override
-    public void onBackPressed() {
-        // Check if we want to block the back button in case of mouse right click.
-        //
-        // If we do, the normal hardware back button will no longer work and people have to use home,
-        // but the mouse right click will work.
-        //
-        boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false);
-        if (trapBack) {
-            // Exit and let the mouse handler handle this button (if appropriate)
-            return;
-        }
-
-        // Default system back button behavior.
-        if (!isFinishing()) {
-            super.onBackPressed();
-        }
-    }
-
-    // Called by JNI from SDL.
-    public static void manualBackButton() {
-        mSingleton.pressBackButton();
-    }
-
-    // Used to get us onto the activity's main thread
-    public void pressBackButton() {
-        runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (!SDLActivity.this.isFinishing()) {
-                    SDLActivity.this.superOnBackPressed();
-                }
-            }
-        });
-    }
-
-    // Used to access the system back behavior.
-    public void superOnBackPressed() {
-        super.onBackPressed();
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(KeyEvent event) {
-
-        if (SDLActivity.mBrokenLibraries) {
-           return false;
-        }
-
-        int keyCode = event.getKeyCode();
-        // Ignore certain special keys so they're handled by Android
-        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
-            keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
-            keyCode == KeyEvent.KEYCODE_CAMERA ||
-            keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */
-            keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */
-            ) {
-            return false;
-        }
-        return super.dispatchKeyEvent(event);
-    }
-
-    /* Transition to next state */
-    public static void handleNativeState() {
-
-        if (mNextNativeState == mCurrentNativeState) {
-            // Already in same state, discard.
-            return;
-        }
-
-        // Try a transition to init state
-        if (mNextNativeState == NativeState.INIT) {
-
-            mCurrentNativeState = mNextNativeState;
-            return;
-        }
-
-        // Try a transition to paused state
-        if (mNextNativeState == NativeState.PAUSED) {
-            if (mSDLThread != null) {
-                nativePause();
-            }
-            if (mSurface != null) {
-                mSurface.handlePause();
-            }
-            mCurrentNativeState = mNextNativeState;
-            return;
-        }
-
-        // Try a transition to resumed state
-        if (mNextNativeState == NativeState.RESUMED) {
-            if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) {
-                if (mSDLThread == null) {
-                    // This is the entry point to the C app.
-                    // Start up the C app thread and enable sensor input for the first time
-                    // FIXME: Why aren't we enabling sensor input at start?
-
-                    mSDLThread = new Thread(new SDLMain(), "SDLThread");
-                    mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true);
-                    mSDLThread.start();
-
-                    // No nativeResume(), don't signal Android_ResumeSem
-                } else {
-                    nativeResume();
-                }
-                mSurface.handleResume();
-
-                mCurrentNativeState = mNextNativeState;
-            }
-        }
-    }
-
-    // Messages from the SDLMain thread
-    static final int COMMAND_CHANGE_TITLE = 1;
-    static final int COMMAND_CHANGE_WINDOW_STYLE = 2;
-    static final int COMMAND_TEXTEDIT_HIDE = 3;
-    static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
-
-    protected static final int COMMAND_USER = 0x8000;
-
-    protected static boolean mFullscreenModeActive;
-
-    /**
-     * This method is called by SDL if SDL did not handle a message itself.
-     * This happens if a received message contains an unsupported command.
-     * Method can be overwritten to handle Messages in a different class.
-     * @param command the command of the message.
-     * @param param the parameter of the message. May be null.
-     * @return if the message was handled in overridden method.
-     */
-    protected boolean onUnhandledMessage(int command, Object param) {
-        return false;
-    }
-
-    /**
-     * A Handler class for Messages from native SDL applications.
-     * It uses current Activities as target (e.g. for the title).
-     * static to prevent implicit references to enclosing object.
-     */
-    protected static class SDLCommandHandler extends Handler {
-        @Override
-        public void handleMessage(Message msg) {
-            Context context = SDL.getContext();
-            if (context == null) {
-                Log.e(TAG, "error handling message, getContext() returned null");
-                return;
-            }
-            switch (msg.arg1) {
-            case COMMAND_CHANGE_TITLE:
-                if (context instanceof Activity) {
-                    ((Activity) context).setTitle((String)msg.obj);
-                } else {
-                    Log.e(TAG, "error handling message, getContext() returned no Activity");
-                }
-                break;
-            case COMMAND_CHANGE_WINDOW_STYLE:
-                if (Build.VERSION.SDK_INT >= 19) {
-                    if (context instanceof Activity) {
-                        Window window = ((Activity) context).getWindow();
-                        if (window != null) {
-                            if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) {
-                                int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
-                                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
-                                        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
-                                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-                                        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-                                        View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
-                                window.getDecorView().setSystemUiVisibility(flags);
-                                window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
-                                window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
-                                SDLActivity.mFullscreenModeActive = true;
-                            } else {
-                                int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE;
-                                window.getDecorView().setSystemUiVisibility(flags);
-                                window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
-                                window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
-                                SDLActivity.mFullscreenModeActive = false;
-                            }
-                        }
-                    } else {
-                        Log.e(TAG, "error handling message, getContext() returned no Activity");
-                    }
-                }
-                break;
-            case COMMAND_TEXTEDIT_HIDE:
-                if (mTextEdit != null) {
-                    // Note: On some devices setting view to GONE creates a flicker in landscape.
-                    // Setting the View's sizes to 0 is similar to GONE but without the flicker.
-                    // The sizes will be set to useful values when the keyboard is shown again.
-                    mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));
-
-                    InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
-                    imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
-
-                    mScreenKeyboardShown = false;
-
-                    mSurface.requestFocus();
-                }
-                break;
-            case COMMAND_SET_KEEP_SCREEN_ON:
-            {
-                if (context instanceof Activity) {
-                    Window window = ((Activity) context).getWindow();
-                    if (window != null) {
-                        if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) {
-                            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-                        } else {
-                            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-                        }
-                    }
-                }
-                break;
-            }
-            default:
-                if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
-                    Log.e(TAG, "error handling message, command is " + msg.arg1);
-                }
-            }
-        }
-    }
-
-    // Handler for the messages
-    Handler commandHandler = new SDLCommandHandler();
-
-    // Send a message from the SDLMain thread
-    boolean sendCommand(int command, Object data) {
-        Message msg = commandHandler.obtainMessage();
-        msg.arg1 = command;
-        msg.obj = data;
-        boolean result = commandHandler.sendMessage(msg);
-
-        if (Build.VERSION.SDK_INT >= 19) {
-            if (command == COMMAND_CHANGE_WINDOW_STYLE) {
-                // Ensure we don't return until the resize has actually happened,
-                // or 500ms have passed.
-
-                boolean bShouldWait = false;
-
-                if (data instanceof Integer) {
-                    // Let's figure out if we're already laid out fullscreen or not.
-                    Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
-                    DisplayMetrics realMetrics = new DisplayMetrics();
-                    display.getRealMetrics(realMetrics);
-
-                    boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) &&
-                            (realMetrics.heightPixels == mSurface.getHeight()));
-
-                    if ((Integer) data == 1) {
-                        // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going
-                        // to change size and should wait for surfaceChanged() before we return, so the size
-                        // is right back in native code.  If we're already laid out fullscreen, though, we're
-                        // not going to change size even if we change decor modes, so we shouldn't wait for
-                        // surfaceChanged() -- which may not even happen -- and should return immediately.
-                        bShouldWait = !bFullscreenLayout;
-                    } else {
-                        // If we're laid out fullscreen (even if the status bar and nav bar are present),
-                        // or are actively in fullscreen, we're going to change size and should wait for
-                        // surfaceChanged before we return, so the size is right back in native code.
-                        bShouldWait = bFullscreenLayout;
-                    }
-                }
-
-                if (bShouldWait && (SDLActivity.getContext() != null)) {
-                    // We'll wait for the surfaceChanged() method, which will notify us
-                    // when called.  That way, we know our current size is really the
-                    // size we need, instead of grabbing a size that's still got
-                    // the navigation and/or status bars before they're hidden.
-                    //
-                    // We'll wait for up to half a second, because some devices
-                    // take a surprisingly long time for the surface resize, but
-                    // then we'll just give up and return.
-                    //
-                    synchronized (SDLActivity.getContext()) {
-                        try {
-                            SDLActivity.getContext().wait(500);
-                        } catch (InterruptedException ie) {
-                            ie.printStackTrace();
-                        }
-                    }
-                }
-            }
-        }
-
-        return result;
-    }
-
-    // C functions we call
-    public static native String nativeGetVersion();
-    public static native int nativeSetupJNI();
-    public static native int nativeRunMain(String library, String function, Object arguments);
-    public static native void nativeLowMemory();
-    public static native void nativeSendQuit();
-    public static native void nativeQuit();
-    public static native void nativePause();
-    public static native void nativeResume();
-    public static native void nativeFocusChanged(boolean hasFocus);
-    public static native void onNativeDropFile(String filename);
-    public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate);
-    public static native void onNativeResize();
-    public static native void onNativeKeyDown(int keycode);
-    public static native void onNativeKeyUp(int keycode);
-    public static native boolean onNativeSoftReturnKey();
-    public static native void onNativeKeyboardFocusLost();
-    public static native void onNativeMouse(int button, int action, float x, float y, boolean relative);
-    public static native void onNativeTouch(int touchDevId, int pointerFingerId,
-                                            int action, float x,
-                                            float y, float p);
-    public static native void onNativeAccel(float x, float y, float z);
-    public static native void onNativeClipboardChanged();
-    public static native void onNativeSurfaceCreated();
-    public static native void onNativeSurfaceChanged();
-    public static native void onNativeSurfaceDestroyed();
-    public static native String nativeGetHint(String name);
-    public static native boolean nativeGetHintBoolean(String name, boolean default_value);
-    public static native void nativeSetenv(String name, String value);
-    public static native void onNativeOrientationChanged(int orientation);
-    public static native void nativeAddTouch(int touchId, String name);
-    public static native void nativePermissionResult(int requestCode, boolean result);
-    public static native void onNativeLocaleChanged();
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean setActivityTitle(String title) {
-        // Called from SDLMain() thread and can't directly affect the view
-        return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void setWindowStyle(boolean fullscreen) {
-        // Called from SDLMain() thread and can't directly affect the view
-        mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     * This is a static method for JNI convenience, it calls a non-static method
-     * so that is can be overridden
-     */
-    public static void setOrientation(int w, int h, boolean resizable, String hint)
-    {
-        if (mSingleton != null) {
-            mSingleton.setOrientationBis(w, h, resizable, hint);
-        }
-    }
-
-    /**
-     * This can be overridden
-     */
-    public void setOrientationBis(int w, int h, boolean resizable, String hint)
-    {
-        int orientation_landscape = -1;
-        int orientation_portrait = -1;
-
-        /* If set, hint "explicitly controls which UI orientations are allowed". */
-        if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) {
-            orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
-        } else if (hint.contains("LandscapeRight")) {
-            orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-        } else if (hint.contains("LandscapeLeft")) {
-            orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-        }
-
-        if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) {
-            orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
-        } else if (hint.contains("Portrait")) {
-            orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-        } else if (hint.contains("PortraitUpsideDown")) {
-            orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
-        }
-
-        boolean is_landscape_allowed = (orientation_landscape != -1);
-        boolean is_portrait_allowed = (orientation_portrait != -1);
-        int req; /* Requested orientation */
-
-        /* No valid hint, nothing is explicitly allowed */
-        if (!is_portrait_allowed && !is_landscape_allowed) {
-            if (resizable) {
-                /* All orientations are allowed */
-                req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
-            } else {
-                /* Fixed window and nothing specified. Get orientation from w/h of created window */
-                req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
-            }
-        } else {
-            /* At least one orientation is allowed */
-            if (resizable) {
-                if (is_portrait_allowed && is_landscape_allowed) {
-                    /* hint allows both landscape and portrait, promote to full sensor */
-                    req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
-                } else {
-                    /* Use the only one allowed "orientation" */
-                    req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
-                }
-            } else {
-                /* Fixed window and both orientations are allowed. Choose one. */
-                if (is_portrait_allowed && is_landscape_allowed) {
-                    req = (w > h ? orientation_landscape : orientation_portrait);
-                } else {
-                    /* Use the only one allowed "orientation" */
-                    req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
-                }
-            }
-        }
-
-        Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint);
-        mSingleton.setRequestedOrientation(req);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void minimizeWindow() {
-
-        if (mSingleton == null) {
-            return;
-        }
-
-        Intent startMain = new Intent(Intent.ACTION_MAIN);
-        startMain.addCategory(Intent.CATEGORY_HOME);
-        startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mSingleton.startActivity(startMain);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean shouldMinimizeOnFocusLoss() {
-/*
-        if (Build.VERSION.SDK_INT >= 24) {
-            if (mSingleton == null) {
-                return true;
-            }
-
-            if (mSingleton.isInMultiWindowMode()) {
-                return false;
-            }
-
-            if (mSingleton.isInPictureInPictureMode()) {
-                return false;
-            }
-        }
-
-        return true;
-*/
-        return false;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean isScreenKeyboardShown()
-    {
-        if (mTextEdit == null) {
-            return false;
-        }
-
-        if (!mScreenKeyboardShown) {
-            return false;
-        }
-
-        InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
-        return imm.isAcceptingText();
-
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean supportsRelativeMouse()
-    {
-        // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under
-        // Android 7 APIs, and simply returns no data under Android 8 APIs.
-        //
-        // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and
-        // thus SDK version 27.  If we are in DeX mode and not API 27 or higher, as a result,
-        // we should stick to relative mode.
-        //
-        if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
-            return false;
-        }
-
-        return SDLActivity.getMotionListener().supportsRelativeMouse();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean setRelativeMouseEnabled(boolean enabled)
-    {
-        if (enabled && !supportsRelativeMouse()) {
-            return false;
-        }
-
-        return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean sendMessage(int command, int param) {
-        if (mSingleton == null) {
-            return false;
-        }
-        return mSingleton.sendCommand(command, param);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static Context getContext() {
-        return SDL.getContext();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean isAndroidTV() {
-        UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE);
-        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
-            return true;
-        }
-        if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) {
-            return true;
-        }
-        if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) {
-            return true;
-        }
-        return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV");
-    }
-
-    public static double getDiagonal()
-    {
-        DisplayMetrics metrics = new DisplayMetrics();
-        Activity activity = (Activity)getContext();
-        if (activity == null) {
-            return 0.0;
-        }
-        activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
-
-        double dWidthInches = metrics.widthPixels / (double)metrics.xdpi;
-        double dHeightInches = metrics.heightPixels / (double)metrics.ydpi;
-
-        return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches));
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean isTablet() {
-        // If our diagonal size is seven inches or greater, we consider ourselves a tablet.
-        return (getDiagonal() >= 7.0);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean isChromebook() {
-        if (getContext() == null) {
-            return false;
-        }
-        return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean isDeXMode() {
-        if (Build.VERSION.SDK_INT < 24) {
-            return false;
-        }
-        try {
-            final Configuration config = getContext().getResources().getConfiguration();
-            final Class<?> configClass = config.getClass();
-            return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
-                    == configClass.getField("semDesktopModeEnabled").getInt(config);
-        } catch(Exception ignored) {
-            return false;
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static DisplayMetrics getDisplayDPI() {
-        return getContext().getResources().getDisplayMetrics();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean getManifestEnvironmentVariables() {
-        try {
-            if (getContext() == null) {
-                return false;
-            }
-
-            ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
-            Bundle bundle = applicationInfo.metaData;
-            if (bundle == null) {
-                return false;
-            }
-            String prefix = "SDL_ENV.";
-            final int trimLength = prefix.length();
-            for (String key : bundle.keySet()) {
-                if (key.startsWith(prefix)) {
-                    String name = key.substring(trimLength);
-                    String value = bundle.get(key).toString();
-                    nativeSetenv(name, value);
-                }
-            }
-            /* environment variables set! */
-            return true;
-        } catch (Exception e) {
-           Log.v(TAG, "exception " + e.toString());
-        }
-        return false;
-    }
-
-    // This method is called by SDLControllerManager's API 26 Generic Motion Handler.
-    public static View getContentView() {
-        return mLayout;
-    }
-
-    static class ShowTextInputTask implements Runnable {
-        /*
-         * This is used to regulate the pan&scan method to have some offset from
-         * the bottom edge of the input region and the top edge of an input
-         * method (soft keyboard)
-         */
-        static final int HEIGHT_PADDING = 15;
-
-        public int x, y, w, h;
-
-        public ShowTextInputTask(int x, int y, int w, int h) {
-            this.x = x;
-            this.y = y;
-            this.w = w;
-            this.h = h;
-
-            /* Minimum size of 1 pixel, so it takes focus. */
-            if (this.w <= 0) {
-                this.w = 1;
-            }
-            if (this.h + HEIGHT_PADDING <= 0) {
-                this.h = 1 - HEIGHT_PADDING;
-            }
-        }
-
-        @Override
-        public void run() {
-            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
-            params.leftMargin = x;
-            params.topMargin = y;
-
-            if (mTextEdit == null) {
-                mTextEdit = new DummyEdit(SDL.getContext());
-
-                mLayout.addView(mTextEdit, params);
-            } else {
-                mTextEdit.setLayoutParams(params);
-            }
-
-            mTextEdit.setVisibility(View.VISIBLE);
-            mTextEdit.requestFocus();
-
-            InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
-            imm.showSoftInput(mTextEdit, 0);
-
-            mScreenKeyboardShown = true;
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean showTextInput(int x, int y, int w, int h) {
-        // Transfer the task to the main thread as a Runnable
-        return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
-    }
-
-    public static boolean isTextInputEvent(KeyEvent event) {
-
-        // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
-        if (event.isCtrlPressed()) {
-            return false;
-        }
-
-        return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE;
-    }
-
-    public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) {
-        int deviceId = event.getDeviceId();
-        int source = event.getSource();
-
-        if (source == InputDevice.SOURCE_UNKNOWN) {
-            InputDevice device = InputDevice.getDevice(deviceId);
-            if (device != null) {
-                source = device.getSources();
-            }
-        }
-
-//        if (event.getAction() == KeyEvent.ACTION_DOWN) {
-//            Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
-//        } else if (event.getAction() == KeyEvent.ACTION_UP) {
-//            Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
-//        }
-
-        // Dispatch the different events depending on where they come from
-        // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
-        // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
-        //
-        // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
-        // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
-        // So, retrieve the device itself and check all of its sources
-        if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
-            // Note that we process events with specific key codes here
-            if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
-                    return true;
-                }
-            } else if (event.getAction() == KeyEvent.ACTION_UP) {
-                if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
-                    return true;
-                }
-            }
-        }
-
-        if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
-            if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                if (isTextInputEvent(event)) {
-                    if (ic != null) {
-                        ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
-                    } else {
-                        SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
-                    }
-                }
-                onNativeKeyDown(keyCode);
-                return true;
-            } else if (event.getAction() == KeyEvent.ACTION_UP) {
-                onNativeKeyUp(keyCode);
-                return true;
-            }
-        }
-
-        if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
-            // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
-            // they are ignored here because sending them as mouse input to SDL is messy
-            if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
-                switch (event.getAction()) {
-                case KeyEvent.ACTION_DOWN:
-                case KeyEvent.ACTION_UP:
-                    // mark the event as handled or it will be handled by system
-                    // handling KEYCODE_BACK by system will call onBackPressed()
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static Surface getNativeSurface() {
-        if (SDLActivity.mSurface == null) {
-            return null;
-        }
-        return SDLActivity.mSurface.getNativeSurface();
-    }
-
-    // Input
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void initTouch() {
-        int[] ids = InputDevice.getDeviceIds();
-
-        for (int id : ids) {
-            InputDevice device = InputDevice.getDevice(id);
-            /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */
-            if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN
-                    || device.isVirtual())) {
-
-                int touchDevId = device.getId();
-                /*
-                 * Prevent id to be -1, since it's used in SDL internal for synthetic events
-                 * Appears when using Android emulator, eg:
-                 *  adb shell input mouse tap 100 100
-                 *  adb shell input touchscreen tap 100 100
-                 */
-                if (touchDevId < 0) {
-                    touchDevId -= 1;
-                }
-                nativeAddTouch(touchDevId, device.getName());
-            }
-        }
-    }
-
-    // Messagebox
-
-    /** Result of current messagebox. Also used for blocking the calling thread. */
-    protected final int[] messageboxSelection = new int[1];
-
-    /**
-     * This method is called by SDL using JNI.
-     * Shows the messagebox from UI thread and block calling thread.
-     * buttonFlags, buttonIds and buttonTexts must have same length.
-     * @param buttonFlags array containing flags for every button.
-     * @param buttonIds array containing id for every button.
-     * @param buttonTexts array containing text for every button.
-     * @param colors null for default or array of length 5 containing colors.
-     * @return button id or -1.
-     */
-    public int messageboxShowMessageBox(
-            final int flags,
-            final String title,
-            final String message,
-            final int[] buttonFlags,
-            final int[] buttonIds,
-            final String[] buttonTexts,
-            final int[] colors) {
-
-        messageboxSelection[0] = -1;
-
-        // sanity checks
-
-        if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
-            return -1; // implementation broken
-        }
-
-        // collect arguments for Dialog
-
-        final Bundle args = new Bundle();
-        args.putInt("flags", flags);
-        args.putString("title", title);
-        args.putString("message", message);
-        args.putIntArray("buttonFlags", buttonFlags);
-        args.putIntArray("buttonIds", buttonIds);
-        args.putStringArray("buttonTexts", buttonTexts);
-        args.putIntArray("colors", colors);
-
-        // trigger Dialog creation on UI thread
-
-        runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                messageboxCreateAndShow(args);
-            }
-        });
-
-        // block the calling thread
-
-        synchronized (messageboxSelection) {
-            try {
-                messageboxSelection.wait();
-            } catch (InterruptedException ex) {
-                ex.printStackTrace();
-                return -1;
-            }
-        }
-
-        // return selected value
-
-        return messageboxSelection[0];
-    }
-
-    protected void messageboxCreateAndShow(Bundle args) {
-
-        // TODO set values from "flags" to messagebox dialog
-
-        // get colors
-
-        int[] colors = args.getIntArray("colors");
-        int backgroundColor;
-        int textColor;
-        int buttonBorderColor;
-        int buttonBackgroundColor;
-        int buttonSelectedColor;
-        if (colors != null) {
-            int i = -1;
-            backgroundColor = colors[++i];
-            textColor = colors[++i];
-            buttonBorderColor = colors[++i];
-            buttonBackgroundColor = colors[++i];
-            buttonSelectedColor = colors[++i];
-        } else {
-            backgroundColor = Color.TRANSPARENT;
-            textColor = Color.TRANSPARENT;
-            buttonBorderColor = Color.TRANSPARENT;
-            buttonBackgroundColor = Color.TRANSPARENT;
-            buttonSelectedColor = Color.TRANSPARENT;
-        }
-
-        // create dialog with title and a listener to wake up calling thread
-
-        final AlertDialog dialog = new AlertDialog.Builder(this).create();
-        dialog.setTitle(args.getString("title"));
-        dialog.setCancelable(false);
-        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
-            @Override
-            public void onDismiss(DialogInterface unused) {
-                synchronized (messageboxSelection) {
-                    messageboxSelection.notify();
-                }
-            }
-        });
-
-        // create text
-
-        TextView message = new TextView(this);
-        message.setGravity(Gravity.CENTER);
-        message.setText(args.getString("message"));
-        if (textColor != Color.TRANSPARENT) {
-            message.setTextColor(textColor);
-        }
-
-        // create buttons
-
-        int[] buttonFlags = args.getIntArray("buttonFlags");
-        int[] buttonIds = args.getIntArray("buttonIds");
-        String[] buttonTexts = args.getStringArray("buttonTexts");
-
-        final SparseArray<Button> mapping = new SparseArray<Button>();
-
-        LinearLayout buttons = new LinearLayout(this);
-        buttons.setOrientation(LinearLayout.HORIZONTAL);
-        buttons.setGravity(Gravity.CENTER);
-        for (int i = 0; i < buttonTexts.length; ++i) {
-            Button button = new Button(this);
-            final int id = buttonIds[i];
-            button.setOnClickListener(new View.OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    messageboxSelection[0] = id;
-                    dialog.dismiss();
-                }
-            });
-            if (buttonFlags[i] != 0) {
-                // see SDL_messagebox.h
-                if ((buttonFlags[i] & 0x00000001) != 0) {
-                    mapping.put(KeyEvent.KEYCODE_ENTER, button);
-                }
-                if ((buttonFlags[i] & 0x00000002) != 0) {
-                    mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */
-                }
-            }
-            button.setText(buttonTexts[i]);
-            if (textColor != Color.TRANSPARENT) {
-                button.setTextColor(textColor);
-            }
-            if (buttonBorderColor != Color.TRANSPARENT) {
-                // TODO set color for border of messagebox button
-            }
-            if (buttonBackgroundColor != Color.TRANSPARENT) {
-                Drawable drawable = button.getBackground();
-                if (drawable == null) {
-                    // setting the color this way removes the style
-                    button.setBackgroundColor(buttonBackgroundColor);
-                } else {
-                    // setting the color this way keeps the style (gradient, padding, etc.)
-                    drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
-                }
-            }
-            if (buttonSelectedColor != Color.TRANSPARENT) {
-                // TODO set color for selected messagebox button
-            }
-            buttons.addView(button);
-        }
-
-        // create content
-
-        LinearLayout content = new LinearLayout(this);
-        content.setOrientation(LinearLayout.VERTICAL);
-        content.addView(message);
-        content.addView(buttons);
-        if (backgroundColor != Color.TRANSPARENT) {
-            content.setBackgroundColor(backgroundColor);
-        }
-
-        // add content to dialog and return
-
-        dialog.setView(content);
-        dialog.setOnKeyListener(new Dialog.OnKeyListener() {
-            @Override
-            public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
-                Button button = mapping.get(keyCode);
-                if (button != null) {
-                    if (event.getAction() == KeyEvent.ACTION_UP) {
-                        button.performClick();
-                    }
-                    return true; // also for ignored actions
-                }
-                return false;
-            }
-        });
-
-        dialog.show();
-    }
-
-    private final Runnable rehideSystemUi = new Runnable() {
-        @Override
-        public void run() {
-            if (Build.VERSION.SDK_INT >= 19) {
-                int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
-                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
-                        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
-                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-                        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-                        View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
-
-                SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags);
-            }
-        }
-    };
-
-    public void onSystemUiVisibilityChange(int visibility) {
-        if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) {
-
-            Handler handler = getWindow().getDecorView().getHandler();
-            if (handler != null) {
-                handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop.
-                handler.postDelayed(rehideSystemUi, 2000);
-            }
-
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean clipboardHasText() {
-        return mClipboardHandler.clipboardHasText();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static String clipboardGetText() {
-        return mClipboardHandler.clipboardGetText();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void clipboardSetText(String string) {
-        mClipboardHandler.clipboardSetText(string);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) {
-        Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
-        ++mLastCursorID;
-
-        if (Build.VERSION.SDK_INT >= 24) {
-            try {
-                mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY));
-            } catch (Exception e) {
-                return 0;
-            }
-        } else {
-            return 0;
-        }
-        return mLastCursorID;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void destroyCustomCursor(int cursorID) {
-        if (Build.VERSION.SDK_INT >= 24) {
-            try {
-                mCursors.remove(cursorID);
-            } catch (Exception e) {
-            }
-        }
-        return;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean setCustomCursor(int cursorID) {
-
-        if (Build.VERSION.SDK_INT >= 24) {
-            try {
-                mSurface.setPointerIcon(mCursors.get(cursorID));
-            } catch (Exception e) {
-                return false;
-            }
-        } else {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static boolean setSystemCursor(int cursorID) {
-        int cursor_type = 0; //PointerIcon.TYPE_NULL;
-        switch (cursorID) {
-        case SDL_SYSTEM_CURSOR_ARROW:
-            cursor_type = 1000; //PointerIcon.TYPE_ARROW;
-            break;
-        case SDL_SYSTEM_CURSOR_IBEAM:
-            cursor_type = 1008; //PointerIcon.TYPE_TEXT;
-            break;
-        case SDL_SYSTEM_CURSOR_WAIT:
-            cursor_type = 1004; //PointerIcon.TYPE_WAIT;
-            break;
-        case SDL_SYSTEM_CURSOR_CROSSHAIR:
-            cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR;
-            break;
-        case SDL_SYSTEM_CURSOR_WAITARROW:
-            cursor_type = 1004; //PointerIcon.TYPE_WAIT;
-            break;
-        case SDL_SYSTEM_CURSOR_SIZENWSE:
-            cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
-            break;
-        case SDL_SYSTEM_CURSOR_SIZENESW:
-            cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
-            break;
-        case SDL_SYSTEM_CURSOR_SIZEWE:
-            cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
-            break;
-        case SDL_SYSTEM_CURSOR_SIZENS:
-            cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
-            break;
-        case SDL_SYSTEM_CURSOR_SIZEALL:
-            cursor_type = 1020; //PointerIcon.TYPE_GRAB;
-            break;
-        case SDL_SYSTEM_CURSOR_NO:
-            cursor_type = 1012; //PointerIcon.TYPE_NO_DROP;
-            break;
-        case SDL_SYSTEM_CURSOR_HAND:
-            cursor_type = 1002; //PointerIcon.TYPE_HAND;
-            break;
-        }
-        if (Build.VERSION.SDK_INT >= 24) {
-            try {
-                mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type));
-            } catch (Exception e) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void requestPermission(String permission, int requestCode) {
-        if (Build.VERSION.SDK_INT < 23) {
-            nativePermissionResult(requestCode, true);
-            return;
-        }
-
-        Activity activity = (Activity)getContext();
-        if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
-            activity.requestPermissions(new String[]{permission}, requestCode);
-        } else {
-            nativePermissionResult(requestCode, true);
-        }
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
-        boolean result = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED);
-        nativePermissionResult(requestCode, result);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int openURL(String url)
-    {
-        try {
-            Intent i = new Intent(Intent.ACTION_VIEW);
-            i.setData(Uri.parse(url));
-
-            int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
-            if (Build.VERSION.SDK_INT >= 21) {
-                flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
-            } else {
-                flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
-            }
-            i.addFlags(flags);
-
-            mSingleton.startActivity(i);
-        } catch (Exception ex) {
-            return -1;
-        }
-        return 0;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset)
-    {
-        if(null == mSingleton) {
-            return - 1;
-        }
-
-        try
-        {
-            class OneShotTask implements Runnable {
-                String mMessage;
-                int mDuration;
-                int mGravity;
-                int mXOffset;
-                int mYOffset;
-
-                OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) {
-                    mMessage  = message;
-                    mDuration = duration;
-                    mGravity  = gravity;
-                    mXOffset  = xOffset;
-                    mYOffset  = yOffset;
-                }
-
-                public void run() {
-                    try
-                    {
-                        Toast toast = Toast.makeText(mSingleton, mMessage, mDuration);
-                        if (mGravity >= 0) {
-                            toast.setGravity(mGravity, mXOffset, mYOffset);
-                        }
-                        toast.show();
-                    } catch(Exception ex) {
-                        Log.e(TAG, ex.getMessage());
-                    }
-                }
-            }
-            mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset));
-        } catch(Exception ex) {
-            return -1;
-        }
-        return 0;
-    }
-}
-
-/**
-    Simple runnable to start the SDL application
-*/
-class SDLMain implements Runnable {
-    @Override
-    public void run() {
-        // Runs SDL_main()
-        String library = SDLActivity.mSingleton.getMainSharedObject();
-        String function = SDLActivity.mSingleton.getMainFunction();
-        String[] arguments = SDLActivity.mSingleton.getArguments();
-
-        try {
-            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
-        } catch (Exception e) {
-            Log.v("SDL", "modify thread properties failed " + e.toString());
-        }
-
-        Log.v("SDL", "Running main function " + function + " from library " + library);
-
-        SDLActivity.nativeRunMain(library, function, arguments);
-
-        Log.v("SDL", "Finished main function");
-
-        if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) {
-            // Let's finish the Activity
-            SDLActivity.mSDLThread = null;
-            SDLActivity.mSingleton.finish();
-        }  // else: Activity is already being destroyed
-
-    }
-}
-
-/* This is a fake invisible editor view that receives the input and defines the
- * pan&scan region
- */
-class DummyEdit extends View implements View.OnKeyListener {
-    InputConnection ic;
-
-    public DummyEdit(Context context) {
-        super(context);
-        setFocusableInTouchMode(true);
-        setFocusable(true);
-        setOnKeyListener(this);
-    }
-
-    @Override
-    public boolean onCheckIsTextEditor() {
-        return true;
-    }
-
-    @Override
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
-    }
-
-    //
-    @Override
-    public boolean onKeyPreIme (int keyCode, KeyEvent event) {
-        // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
-        // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
-        // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
-        // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
-        // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
-        // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
-        if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
-            if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
-                SDLActivity.onNativeKeyboardFocusLost();
-            }
-        }
-        return super.onKeyPreIme(keyCode, event);
-    }
-
-    @Override
-    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
-        ic = new SDLInputConnection(this, true);
-
-        outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
-                             InputType.TYPE_TEXT_FLAG_MULTI_LINE;
-        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
-                              EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
-
-        return ic;
-    }
-}
-
-class SDLInputConnection extends BaseInputConnection {
-
-    protected EditText mEditText;
-    protected String mCommittedText = "";
-
-    public SDLInputConnection(View targetView, boolean fullEditor) {
-        super(targetView, fullEditor);
-        mEditText = new EditText(SDL.getContext());
-    }
-
-    @Override
-    public Editable getEditable() {
-        return mEditText.getEditableText();
-    }
-
-    @Override
-    public boolean sendKeyEvent(KeyEvent event) {
-        /*
-         * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
-         * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
-         * and so we need to generate them ourselves in commitText.  To avoid duplicates on the handful of keys
-         * that still do, we empty this out.
-         */
-
-        /*
-         * Return DOES still generate a key event, however.  So rather than using it as the 'click a button' key
-         * as we do with physical keyboards, let's just use it to hide the keyboard.
-         */
-
-        if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
-            if (SDLActivity.onNativeSoftReturnKey()) {
-                return true;
-            }
-        }
-
-        return super.sendKeyEvent(event);
-    }
-
-    @Override
-    public boolean commitText(CharSequence text, int newCursorPosition) {
-        if (!super.commitText(text, newCursorPosition)) {
-            return false;
-        }
-        updateText();
-        return true;
-    }
-
-    @Override
-    public boolean setComposingText(CharSequence text, int newCursorPosition) {
-        if (!super.setComposingText(text, newCursorPosition)) {
-            return false;
-        }
-        updateText();
-        return true;
-    }
-
-    @Override
-    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
-        if (!super.deleteSurroundingText(beforeLength, afterLength)) {
-            return false;
-        }
-        updateText();
-        return true;
-    }
-
-    protected void updateText() {
-        final Editable content = getEditable();
-        if (content == null) {
-            return;
-        }
-
-        String text = content.toString();
-        int compareLength = Math.min(text.length(), mCommittedText.length());
-        int matchLength, offset;
-
-        /* Backspace over characters that are no longer in the string */
-        for (matchLength = 0; matchLength < compareLength; ) {
-            int codePoint = mCommittedText.codePointAt(matchLength);
-            if (codePoint != text.codePointAt(matchLength)) {
-                break;
-            }
-            matchLength += Character.charCount(codePoint);
-        }
-        /* FIXME: This doesn't handle graphemes, like '🌬️' */
-        for (offset = matchLength; offset < mCommittedText.length(); ) {
-            int codePoint = mCommittedText.codePointAt(offset);
-            nativeGenerateScancodeForUnichar('\b');
-            offset += Character.charCount(codePoint);
-        }
-
-        if (matchLength < text.length()) {
-            String pendingText = text.subSequence(matchLength, text.length()).toString();
-            for (offset = 0; offset < pendingText.length(); ) {
-                int codePoint = pendingText.codePointAt(offset);
-                if (codePoint == '\n') {
-                    if (SDLActivity.onNativeSoftReturnKey()) {
-                        return;
-                    }
-                }
-                /* Higher code points don't generate simulated scancodes */
-                if (codePoint < 128) {
-                    nativeGenerateScancodeForUnichar((char)codePoint);
-                }
-                offset += Character.charCount(codePoint);
-            }
-            SDLInputConnection.nativeCommitText(pendingText, 0);
-        }
-        mCommittedText = text;
-    }
-
-    public static native void nativeCommitText(String text, int newCursorPosition);
-
-    public static native void nativeGenerateScancodeForUnichar(char c);
-}
-
-class SDLClipboardHandler implements
-    ClipboardManager.OnPrimaryClipChangedListener {
-
-    protected ClipboardManager mClipMgr;
-
-    SDLClipboardHandler() {
-       mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
-       mClipMgr.addPrimaryClipChangedListener(this);
-    }
-
-    public boolean clipboardHasText() {
-       return mClipMgr.hasPrimaryClip();
-    }
-
-    public String clipboardGetText() {
-        ClipData clip = mClipMgr.getPrimaryClip();
-        if (clip != null) {
-            ClipData.Item item = clip.getItemAt(0);
-            if (item != null) {
-                CharSequence text = item.getText();
-                if (text != null) {
-                    return text.toString();
-                }
-            }
-        }
-        return null;
-    }
-
-    public void clipboardSetText(String string) {
-       mClipMgr.removePrimaryClipChangedListener(this);
-       ClipData clip = ClipData.newPlainText(null, string);
-       mClipMgr.setPrimaryClip(clip);
-       mClipMgr.addPrimaryClipChangedListener(this);
-    }
-
-    @Override
-    public void onPrimaryClipChanged() {
-        SDLActivity.onNativeClipboardChanged();
-    }
-}
-

+ 0 - 394
android/vcmi-app/src/main/java/org/libsdl/app/SDLAudioManager.java

@@ -1,394 +0,0 @@
-package org.libsdl.app;
-
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.AudioRecord;
-import android.media.AudioTrack;
-import android.media.MediaRecorder;
-import android.os.Build;
-import android.util.Log;
-
-public class SDLAudioManager
-{
-    protected static final String TAG = "SDLAudio";
-
-    protected static AudioTrack mAudioTrack;
-    protected static AudioRecord mAudioRecord;
-
-    public static void initialize() {
-        mAudioTrack = null;
-        mAudioRecord = null;
-    }
-
-    // Audio
-
-    protected static String getAudioFormatString(int audioFormat) {
-        switch (audioFormat) {
-        case AudioFormat.ENCODING_PCM_8BIT:
-            return "8-bit";
-        case AudioFormat.ENCODING_PCM_16BIT:
-            return "16-bit";
-        case AudioFormat.ENCODING_PCM_FLOAT:
-            return "float";
-        default:
-            return Integer.toString(audioFormat);
-        }
-    }
-
-    protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
-        int channelConfig;
-        int sampleSize;
-        int frameSize;
-
-        Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
-
-        /* On older devices let's use known good settings */
-        if (Build.VERSION.SDK_INT < 21) {
-            if (desiredChannels > 2) {
-                desiredChannels = 2;
-            }
-        }
-
-        /* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
-        if (Build.VERSION.SDK_INT < 22) {
-            if (sampleRate < 8000) {
-                sampleRate = 8000;
-            } else if (sampleRate > 48000) {
-                sampleRate = 48000;
-            }
-        }
-
-        if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
-            int minSDKVersion = (isCapture ? 23 : 21);
-            if (Build.VERSION.SDK_INT < minSDKVersion) {
-                audioFormat = AudioFormat.ENCODING_PCM_16BIT;
-            }
-        }
-        switch (audioFormat)
-        {
-        case AudioFormat.ENCODING_PCM_8BIT:
-            sampleSize = 1;
-            break;
-        case AudioFormat.ENCODING_PCM_16BIT:
-            sampleSize = 2;
-            break;
-        case AudioFormat.ENCODING_PCM_FLOAT:
-            sampleSize = 4;
-            break;
-        default:
-            Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
-            audioFormat = AudioFormat.ENCODING_PCM_16BIT;
-            sampleSize = 2;
-            break;
-        }
-
-        if (isCapture) {
-            switch (desiredChannels) {
-            case 1:
-                channelConfig = AudioFormat.CHANNEL_IN_MONO;
-                break;
-            case 2:
-                channelConfig = AudioFormat.CHANNEL_IN_STEREO;
-                break;
-            default:
-                Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
-                desiredChannels = 2;
-                channelConfig = AudioFormat.CHANNEL_IN_STEREO;
-                break;
-            }
-        } else {
-            switch (desiredChannels) {
-            case 1:
-                channelConfig = AudioFormat.CHANNEL_OUT_MONO;
-                break;
-            case 2:
-                channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
-                break;
-            case 3:
-                channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
-                break;
-            case 4:
-                channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
-                break;
-            case 5:
-                channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
-                break;
-            case 6:
-                channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
-                break;
-            case 7:
-                channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
-                break;
-            case 8:
-                if (Build.VERSION.SDK_INT >= 23) {
-                    channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
-                } else {
-                    Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
-                    desiredChannels = 6;
-                    channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
-                }
-                break;
-            default:
-                Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
-                desiredChannels = 2;
-                channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
-                break;
-            }
-
-/*
-            Log.v(TAG, "Speaker configuration (and order of channels):");
-
-            if ((channelConfig & 0x00000004) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_FRONT_LEFT");
-            }
-            if ((channelConfig & 0x00000008) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_FRONT_RIGHT");
-            }
-            if ((channelConfig & 0x00000010) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_FRONT_CENTER");
-            }
-            if ((channelConfig & 0x00000020) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_LOW_FREQUENCY");
-            }
-            if ((channelConfig & 0x00000040) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_BACK_LEFT");
-            }
-            if ((channelConfig & 0x00000080) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_BACK_RIGHT");
-            }
-            if ((channelConfig & 0x00000100) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
-            }
-            if ((channelConfig & 0x00000200) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
-            }
-            if ((channelConfig & 0x00000400) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_BACK_CENTER");
-            }
-            if ((channelConfig & 0x00000800) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_SIDE_LEFT");
-            }
-            if ((channelConfig & 0x00001000) != 0) {
-                Log.v(TAG, "   CHANNEL_OUT_SIDE_RIGHT");
-            }
-*/
-        }
-        frameSize = (sampleSize * desiredChannels);
-
-        // Let the user pick a larger buffer if they really want -- but ye
-        // gods they probably shouldn't, the minimums are horrifyingly high
-        // latency already
-        int minBufferSize;
-        if (isCapture) {
-            minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
-        } else {
-            minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
-        }
-        desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
-
-        int[] results = new int[4];
-
-        if (isCapture) {
-            if (mAudioRecord == null) {
-                mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
-                        channelConfig, audioFormat, desiredFrames * frameSize);
-
-                // see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
-                if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
-                    Log.e(TAG, "Failed during initialization of AudioRecord");
-                    mAudioRecord.release();
-                    mAudioRecord = null;
-                    return null;
-                }
-
-                mAudioRecord.startRecording();
-            }
-
-            results[0] = mAudioRecord.getSampleRate();
-            results[1] = mAudioRecord.getAudioFormat();
-            results[2] = mAudioRecord.getChannelCount();
-
-        } else {
-            if (mAudioTrack == null) {
-                mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
-
-                // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
-                // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
-                // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
-                if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
-                    /* Try again, with safer values */
-
-                    Log.e(TAG, "Failed during initialization of Audio Track");
-                    mAudioTrack.release();
-                    mAudioTrack = null;
-                    return null;
-                }
-
-                mAudioTrack.play();
-            }
-
-            results[0] = mAudioTrack.getSampleRate();
-            results[1] = mAudioTrack.getAudioFormat();
-            results[2] = mAudioTrack.getChannelCount();
-        }
-        results[3] = desiredFrames;
-
-        Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
-
-        return results;
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
-        return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void audioWriteFloatBuffer(float[] buffer) {
-        if (mAudioTrack == null) {
-            Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
-            return;
-        }
-
-        for (int i = 0; i < buffer.length;) {
-            int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
-            if (result > 0) {
-                i += result;
-            } else if (result == 0) {
-                try {
-                    Thread.sleep(1);
-                } catch(InterruptedException e) {
-                    // Nom nom
-                }
-            } else {
-                Log.w(TAG, "SDL audio: error return from write(float)");
-                return;
-            }
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void audioWriteShortBuffer(short[] buffer) {
-        if (mAudioTrack == null) {
-            Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
-            return;
-        }
-
-        for (int i = 0; i < buffer.length;) {
-            int result = mAudioTrack.write(buffer, i, buffer.length - i);
-            if (result > 0) {
-                i += result;
-            } else if (result == 0) {
-                try {
-                    Thread.sleep(1);
-                } catch(InterruptedException e) {
-                    // Nom nom
-                }
-            } else {
-                Log.w(TAG, "SDL audio: error return from write(short)");
-                return;
-            }
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void audioWriteByteBuffer(byte[] buffer) {
-        if (mAudioTrack == null) {
-            Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
-            return;
-        }
-
-        for (int i = 0; i < buffer.length; ) {
-            int result = mAudioTrack.write(buffer, i, buffer.length - i);
-            if (result > 0) {
-                i += result;
-            } else if (result == 0) {
-                try {
-                    Thread.sleep(1);
-                } catch(InterruptedException e) {
-                    // Nom nom
-                }
-            } else {
-                Log.w(TAG, "SDL audio: error return from write(byte)");
-                return;
-            }
-        }
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
-        return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
-        return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
-        if (Build.VERSION.SDK_INT < 23) {
-            return mAudioRecord.read(buffer, 0, buffer.length);
-        } else {
-            return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
-        }
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
-        if (Build.VERSION.SDK_INT < 23) {
-            return mAudioRecord.read(buffer, 0, buffer.length);
-        } else {
-            return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
-        }
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static void audioClose() {
-        if (mAudioTrack != null) {
-            mAudioTrack.stop();
-            mAudioTrack.release();
-            mAudioTrack = null;
-        }
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static void captureClose() {
-        if (mAudioRecord != null) {
-            mAudioRecord.stop();
-            mAudioRecord.release();
-            mAudioRecord = null;
-        }
-    }
-
-    /** This method is called by SDL using JNI. */
-    public static void audioSetThreadPriority(boolean iscapture, int device_id) {
-        try {
-
-            /* Set thread name */
-            if (iscapture) {
-                Thread.currentThread().setName("SDLAudioC" + device_id);
-            } else {
-                Thread.currentThread().setName("SDLAudioP" + device_id);
-            }
-
-            /* Set thread priority */
-            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
-
-        } catch (Exception e) {
-            Log.v(TAG, "modify thread properties failed " + e.toString());
-        }
-    }
-
-    public static native int nativeSetupJNI();
-}

+ 0 - 814
android/vcmi-app/src/main/java/org/libsdl/app/SDLControllerManager.java

@@ -1,814 +0,0 @@
-package org.libsdl.app;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-
-public class SDLControllerManager
-{
-
-    public static native int nativeSetupJNI();
-
-    public static native int nativeAddJoystick(int device_id, String name, String desc,
-                                               int vendor_id, int product_id,
-                                               boolean is_accelerometer, int button_mask,
-                                               int naxes, int nhats, int nballs);
-    public static native int nativeRemoveJoystick(int device_id);
-    public static native int nativeAddHaptic(int device_id, String name);
-    public static native int nativeRemoveHaptic(int device_id);
-    public static native int onNativePadDown(int device_id, int keycode);
-    public static native int onNativePadUp(int device_id, int keycode);
-    public static native void onNativeJoy(int device_id, int axis,
-                                          float value);
-    public static native void onNativeHat(int device_id, int hat_id,
-                                          int x, int y);
-
-    protected static SDLJoystickHandler mJoystickHandler;
-    protected static SDLHapticHandler mHapticHandler;
-
-    private static final String TAG = "SDLControllerManager";
-
-    public static void initialize() {
-        if (mJoystickHandler == null) {
-            if (Build.VERSION.SDK_INT >= 19) {
-                mJoystickHandler = new SDLJoystickHandler_API19();
-            } else {
-                mJoystickHandler = new SDLJoystickHandler_API16();
-            }
-        }
-
-        if (mHapticHandler == null) {
-            if (Build.VERSION.SDK_INT >= 26) {
-                mHapticHandler = new SDLHapticHandler_API26();
-            } else {
-                mHapticHandler = new SDLHapticHandler();
-            }
-        }
-    }
-
-    // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
-    public static boolean handleJoystickMotionEvent(MotionEvent event) {
-        return mJoystickHandler.handleMotionEvent(event);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void pollInputDevices() {
-        mJoystickHandler.pollInputDevices();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void pollHapticDevices() {
-        mHapticHandler.pollHapticDevices();
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void hapticRun(int device_id, float intensity, int length) {
-        mHapticHandler.run(device_id, intensity, length);
-    }
-
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static void hapticStop(int device_id)
-    {
-        mHapticHandler.stop(device_id);
-    }
-
-    // Check if a given device is considered a possible SDL joystick
-    public static boolean isDeviceSDLJoystick(int deviceId) {
-        InputDevice device = InputDevice.getDevice(deviceId);
-        // We cannot use InputDevice.isVirtual before API 16, so let's accept
-        // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
-        if ((device == null) || (deviceId < 0)) {
-            return false;
-        }
-        int sources = device.getSources();
-
-        /* This is called for every button press, so let's not spam the logs */
-        /*
-        if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
-            Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
-        }
-        if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
-            Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
-        }
-        if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
-            Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
-        }
-        */
-
-        return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
-                ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
-                ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
-        );
-    }
-
-}
-
-class SDLJoystickHandler {
-
-    /**
-     * Handles given MotionEvent.
-     * @param event the event to be handled.
-     * @return if given event was processed.
-     */
-    public boolean handleMotionEvent(MotionEvent event) {
-        return false;
-    }
-
-    /**
-     * Handles adding and removing of input devices.
-     */
-    public void pollInputDevices() {
-    }
-}
-
-/* Actual joystick functionality available for API >= 12 devices */
-class SDLJoystickHandler_API16 extends SDLJoystickHandler {
-
-    static class SDLJoystick {
-        public int device_id;
-        public String name;
-        public String desc;
-        public ArrayList<InputDevice.MotionRange> axes;
-        public ArrayList<InputDevice.MotionRange> hats;
-    }
-    static class RangeComparator implements Comparator<InputDevice.MotionRange> {
-        @Override
-        public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
-            // Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
-            int arg0Axis = arg0.getAxis();
-            int arg1Axis = arg1.getAxis();
-            if (arg0Axis == MotionEvent.AXIS_GAS) {
-                arg0Axis = MotionEvent.AXIS_BRAKE;
-            } else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
-                arg0Axis = MotionEvent.AXIS_GAS;
-            }
-            if (arg1Axis == MotionEvent.AXIS_GAS) {
-                arg1Axis = MotionEvent.AXIS_BRAKE;
-            } else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
-                arg1Axis = MotionEvent.AXIS_GAS;
-            }
-
-            // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
-            // This is because the usual pairing are:
-            // - AXIS_X + AXIS_Y (left stick).
-            // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
-            // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
-            // This sorts the axes in the above order, which tends to be correct
-            // for Xbox-ish game pads that have the right stick on RX/RY and the
-            // triggers on Z/RZ.
-            //
-            // Gamepads that don't have AXIS_Z/AXIS_RZ but use
-            // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
-            //
-            // References:
-            // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
-            // - https://www.kernel.org/doc/html/latest/input/gamepad.html
-            if (arg0Axis == MotionEvent.AXIS_Z) {
-                arg0Axis = MotionEvent.AXIS_RZ - 1;
-            } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
-                --arg0Axis;
-            }
-            if (arg1Axis == MotionEvent.AXIS_Z) {
-                arg1Axis = MotionEvent.AXIS_RZ - 1;
-            } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
-                --arg1Axis;
-            }
-
-            return arg0Axis - arg1Axis;
-        }
-    }
-
-    private final ArrayList<SDLJoystick> mJoysticks;
-
-    public SDLJoystickHandler_API16() {
-
-        mJoysticks = new ArrayList<SDLJoystick>();
-    }
-
-    @Override
-    public void pollInputDevices() {
-        int[] deviceIds = InputDevice.getDeviceIds();
-
-        for (int device_id : deviceIds) {
-            if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
-                SDLJoystick joystick = getJoystick(device_id);
-                if (joystick == null) {
-                    InputDevice joystickDevice = InputDevice.getDevice(device_id);
-                    joystick = new SDLJoystick();
-                    joystick.device_id = device_id;
-                    joystick.name = joystickDevice.getName();
-                    joystick.desc = getJoystickDescriptor(joystickDevice);
-                    joystick.axes = new ArrayList<InputDevice.MotionRange>();
-                    joystick.hats = new ArrayList<InputDevice.MotionRange>();
-
-                    List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
-                    Collections.sort(ranges, new RangeComparator());
-                    for (InputDevice.MotionRange range : ranges) {
-                        if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
-                            if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
-                                joystick.hats.add(range);
-                            } else {
-                                joystick.axes.add(range);
-                            }
-                        }
-                    }
-
-                    mJoysticks.add(joystick);
-                    SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
-                            getVendorId(joystickDevice), getProductId(joystickDevice), false,
-                            getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0);
-                }
-            }
-        }
-
-        /* Check removed devices */
-        ArrayList<Integer> removedDevices = null;
-        for (SDLJoystick joystick : mJoysticks) {
-            int device_id = joystick.device_id;
-            int i;
-            for (i = 0; i < deviceIds.length; i++) {
-                if (device_id == deviceIds[i]) break;
-            }
-            if (i == deviceIds.length) {
-                if (removedDevices == null) {
-                    removedDevices = new ArrayList<Integer>();
-                }
-                removedDevices.add(device_id);
-            }
-        }
-
-        if (removedDevices != null) {
-            for (int device_id : removedDevices) {
-                SDLControllerManager.nativeRemoveJoystick(device_id);
-                for (int i = 0; i < mJoysticks.size(); i++) {
-                    if (mJoysticks.get(i).device_id == device_id) {
-                        mJoysticks.remove(i);
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    protected SDLJoystick getJoystick(int device_id) {
-        for (SDLJoystick joystick : mJoysticks) {
-            if (joystick.device_id == device_id) {
-                return joystick;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public boolean handleMotionEvent(MotionEvent event) {
-        int actionPointerIndex = event.getActionIndex();
-        int action = event.getActionMasked();
-        if (action == MotionEvent.ACTION_MOVE) {
-            SDLJoystick joystick = getJoystick(event.getDeviceId());
-            if (joystick != null) {
-                for (int i = 0; i < joystick.axes.size(); i++) {
-                    InputDevice.MotionRange range = joystick.axes.get(i);
-                    /* Normalize the value to -1...1 */
-                    float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
-                    SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
-                }
-                for (int i = 0; i < joystick.hats.size() / 2; i++) {
-                    int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
-                    int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
-                    SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
-                }
-            }
-        }
-        return true;
-    }
-
-    public String getJoystickDescriptor(InputDevice joystickDevice) {
-        String desc = joystickDevice.getDescriptor();
-
-        if (desc != null && !desc.isEmpty()) {
-            return desc;
-        }
-
-        return joystickDevice.getName();
-    }
-    public int getProductId(InputDevice joystickDevice) {
-        return 0;
-    }
-    public int getVendorId(InputDevice joystickDevice) {
-        return 0;
-    }
-    public int getButtonMask(InputDevice joystickDevice) {
-        return -1;
-    }
-}
-
-class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
-
-    @Override
-    public int getProductId(InputDevice joystickDevice) {
-        return joystickDevice.getProductId();
-    }
-
-    @Override
-    public int getVendorId(InputDevice joystickDevice) {
-        return joystickDevice.getVendorId();
-    }
-
-    @Override
-    public int getButtonMask(InputDevice joystickDevice) {
-        int button_mask = 0;
-        int[] keys = new int[] {
-            KeyEvent.KEYCODE_BUTTON_A,
-            KeyEvent.KEYCODE_BUTTON_B,
-            KeyEvent.KEYCODE_BUTTON_X,
-            KeyEvent.KEYCODE_BUTTON_Y,
-            KeyEvent.KEYCODE_BACK,
-            KeyEvent.KEYCODE_MENU,
-            KeyEvent.KEYCODE_BUTTON_MODE,
-            KeyEvent.KEYCODE_BUTTON_START,
-            KeyEvent.KEYCODE_BUTTON_THUMBL,
-            KeyEvent.KEYCODE_BUTTON_THUMBR,
-            KeyEvent.KEYCODE_BUTTON_L1,
-            KeyEvent.KEYCODE_BUTTON_R1,
-            KeyEvent.KEYCODE_DPAD_UP,
-            KeyEvent.KEYCODE_DPAD_DOWN,
-            KeyEvent.KEYCODE_DPAD_LEFT,
-            KeyEvent.KEYCODE_DPAD_RIGHT,
-            KeyEvent.KEYCODE_BUTTON_SELECT,
-            KeyEvent.KEYCODE_DPAD_CENTER,
-
-            // These don't map into any SDL controller buttons directly
-            KeyEvent.KEYCODE_BUTTON_L2,
-            KeyEvent.KEYCODE_BUTTON_R2,
-            KeyEvent.KEYCODE_BUTTON_C,
-            KeyEvent.KEYCODE_BUTTON_Z,
-            KeyEvent.KEYCODE_BUTTON_1,
-            KeyEvent.KEYCODE_BUTTON_2,
-            KeyEvent.KEYCODE_BUTTON_3,
-            KeyEvent.KEYCODE_BUTTON_4,
-            KeyEvent.KEYCODE_BUTTON_5,
-            KeyEvent.KEYCODE_BUTTON_6,
-            KeyEvent.KEYCODE_BUTTON_7,
-            KeyEvent.KEYCODE_BUTTON_8,
-            KeyEvent.KEYCODE_BUTTON_9,
-            KeyEvent.KEYCODE_BUTTON_10,
-            KeyEvent.KEYCODE_BUTTON_11,
-            KeyEvent.KEYCODE_BUTTON_12,
-            KeyEvent.KEYCODE_BUTTON_13,
-            KeyEvent.KEYCODE_BUTTON_14,
-            KeyEvent.KEYCODE_BUTTON_15,
-            KeyEvent.KEYCODE_BUTTON_16,
-        };
-        int[] masks = new int[] {
-            (1 << 0),   // A -> A
-            (1 << 1),   // B -> B
-            (1 << 2),   // X -> X
-            (1 << 3),   // Y -> Y
-            (1 << 4),   // BACK -> BACK
-            (1 << 6),   // MENU -> START
-            (1 << 5),   // MODE -> GUIDE
-            (1 << 6),   // START -> START
-            (1 << 7),   // THUMBL -> LEFTSTICK
-            (1 << 8),   // THUMBR -> RIGHTSTICK
-            (1 << 9),   // L1 -> LEFTSHOULDER
-            (1 << 10),  // R1 -> RIGHTSHOULDER
-            (1 << 11),  // DPAD_UP -> DPAD_UP
-            (1 << 12),  // DPAD_DOWN -> DPAD_DOWN
-            (1 << 13),  // DPAD_LEFT -> DPAD_LEFT
-            (1 << 14),  // DPAD_RIGHT -> DPAD_RIGHT
-            (1 << 4),   // SELECT -> BACK
-            (1 << 0),   // DPAD_CENTER -> A
-            (1 << 15),  // L2 -> ??
-            (1 << 16),  // R2 -> ??
-            (1 << 17),  // C -> ??
-            (1 << 18),  // Z -> ??
-            (1 << 20),  // 1 -> ??
-            (1 << 21),  // 2 -> ??
-            (1 << 22),  // 3 -> ??
-            (1 << 23),  // 4 -> ??
-            (1 << 24),  // 5 -> ??
-            (1 << 25),  // 6 -> ??
-            (1 << 26),  // 7 -> ??
-            (1 << 27),  // 8 -> ??
-            (1 << 28),  // 9 -> ??
-            (1 << 29),  // 10 -> ??
-            (1 << 30),  // 11 -> ??
-            (1 << 31),  // 12 -> ??
-            // We're out of room...
-            0xFFFFFFFF,  // 13 -> ??
-            0xFFFFFFFF,  // 14 -> ??
-            0xFFFFFFFF,  // 15 -> ??
-            0xFFFFFFFF,  // 16 -> ??
-        };
-        boolean[] has_keys = joystickDevice.hasKeys(keys);
-        for (int i = 0; i < keys.length; ++i) {
-            if (has_keys[i]) {
-                button_mask |= masks[i];
-            }
-        }
-        return button_mask;
-    }
-}
-
-class SDLHapticHandler_API26 extends SDLHapticHandler {
-    @Override
-    public void run(int device_id, float intensity, int length) {
-        SDLHaptic haptic = getHaptic(device_id);
-        if (haptic != null) {
-            Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
-            if (intensity == 0.0f) {
-                stop(device_id);
-                return;
-            }
-
-            int vibeValue = Math.round(intensity * 255);
-
-            if (vibeValue > 255) {
-                vibeValue = 255;
-            }
-            if (vibeValue < 1) {
-                stop(device_id);
-                return;
-            }
-            try {
-                haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
-            }
-            catch (Exception e) {
-                // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
-                // something went horribly wrong with the Android 8.0 APIs.
-                haptic.vib.vibrate(length);
-            }
-        }
-    }
-}
-
-class SDLHapticHandler {
-
-    static class SDLHaptic {
-        public int device_id;
-        public String name;
-        public Vibrator vib;
-    }
-
-    private final ArrayList<SDLHaptic> mHaptics;
-
-    public SDLHapticHandler() {
-        mHaptics = new ArrayList<SDLHaptic>();
-    }
-
-    public void run(int device_id, float intensity, int length) {
-        SDLHaptic haptic = getHaptic(device_id);
-        if (haptic != null) {
-            haptic.vib.vibrate(length);
-        }
-    }
-
-    public void stop(int device_id) {
-        SDLHaptic haptic = getHaptic(device_id);
-        if (haptic != null) {
-            haptic.vib.cancel();
-        }
-    }
-
-    public void pollHapticDevices() {
-
-        final int deviceId_VIBRATOR_SERVICE = 999999;
-        boolean hasVibratorService = false;
-
-        int[] deviceIds = InputDevice.getDeviceIds();
-        // It helps processing the device ids in reverse order
-        // For example, in the case of the XBox 360 wireless dongle,
-        // so the first controller seen by SDL matches what the receiver
-        // considers to be the first controller
-
-        for (int i = deviceIds.length - 1; i > -1; i--) {
-            SDLHaptic haptic = getHaptic(deviceIds[i]);
-            if (haptic == null) {
-                InputDevice device = InputDevice.getDevice(deviceIds[i]);
-                Vibrator vib = device.getVibrator();
-                if (vib.hasVibrator()) {
-                    haptic = new SDLHaptic();
-                    haptic.device_id = deviceIds[i];
-                    haptic.name = device.getName();
-                    haptic.vib = vib;
-                    mHaptics.add(haptic);
-                    SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
-                }
-            }
-        }
-
-        /* Check VIBRATOR_SERVICE */
-        Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
-        if (vib != null) {
-            hasVibratorService = vib.hasVibrator();
-
-            if (hasVibratorService) {
-                SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
-                if (haptic == null) {
-                    haptic = new SDLHaptic();
-                    haptic.device_id = deviceId_VIBRATOR_SERVICE;
-                    haptic.name = "VIBRATOR_SERVICE";
-                    haptic.vib = vib;
-                    mHaptics.add(haptic);
-                    SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
-                }
-            }
-        }
-
-        /* Check removed devices */
-        ArrayList<Integer> removedDevices = null;
-        for (SDLHaptic haptic : mHaptics) {
-            int device_id = haptic.device_id;
-            int i;
-            for (i = 0; i < deviceIds.length; i++) {
-                if (device_id == deviceIds[i]) break;
-            }
-
-            if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
-                if (i == deviceIds.length) {
-                    if (removedDevices == null) {
-                        removedDevices = new ArrayList<Integer>();
-                    }
-                    removedDevices.add(device_id);
-                }
-            }  // else: don't remove the vibrator if it is still present
-        }
-
-        if (removedDevices != null) {
-            for (int device_id : removedDevices) {
-                SDLControllerManager.nativeRemoveHaptic(device_id);
-                for (int i = 0; i < mHaptics.size(); i++) {
-                    if (mHaptics.get(i).device_id == device_id) {
-                        mHaptics.remove(i);
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    protected SDLHaptic getHaptic(int device_id) {
-        for (SDLHaptic haptic : mHaptics) {
-            if (haptic.device_id == device_id) {
-                return haptic;
-            }
-        }
-        return null;
-    }
-}
-
-class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
-    // Generic Motion (mouse hover, joystick...) events go here
-    @Override
-    public boolean onGenericMotion(View v, MotionEvent event) {
-        float x, y;
-        int action;
-
-        switch ( event.getSource() ) {
-            case InputDevice.SOURCE_JOYSTICK:
-                return SDLControllerManager.handleJoystickMotionEvent(event);
-
-            case InputDevice.SOURCE_MOUSE:
-                action = event.getActionMasked();
-                switch (action) {
-                    case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
-
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    default:
-                        break;
-                }
-                break;
-
-            default:
-                break;
-        }
-
-        // Event was not managed
-        return false;
-    }
-
-    public boolean supportsRelativeMouse() {
-        return false;
-    }
-
-    public boolean inRelativeMode() {
-        return false;
-    }
-
-    public boolean setRelativeMouseEnabled(boolean enabled) {
-        return false;
-    }
-
-    public void reclaimRelativeMouseModeIfNeeded()
-    {
-
-    }
-
-    public float getEventX(MotionEvent event) {
-        return event.getX(0);
-    }
-
-    public float getEventY(MotionEvent event) {
-        return event.getY(0);
-    }
-
-}
-
-class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
-    // Generic Motion (mouse hover, joystick...) events go here
-
-    private boolean mRelativeModeEnabled;
-
-    @Override
-    public boolean onGenericMotion(View v, MotionEvent event) {
-
-        // Handle relative mouse mode
-        if (mRelativeModeEnabled) {
-            if (event.getSource() == InputDevice.SOURCE_MOUSE) {
-                int action = event.getActionMasked();
-                if (action == MotionEvent.ACTION_HOVER_MOVE) {
-                    float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
-                    float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
-                    SDLActivity.onNativeMouse(0, action, x, y, true);
-                    return true;
-                }
-            }
-        }
-
-        // Event was not managed, call SDLGenericMotionListener_API12 method
-        return super.onGenericMotion(v, event);
-    }
-
-    @Override
-    public boolean supportsRelativeMouse() {
-        return true;
-    }
-
-    @Override
-    public boolean inRelativeMode() {
-        return mRelativeModeEnabled;
-    }
-
-    @Override
-    public boolean setRelativeMouseEnabled(boolean enabled) {
-        mRelativeModeEnabled = enabled;
-        return true;
-    }
-
-    @Override
-    public float getEventX(MotionEvent event) {
-        if (mRelativeModeEnabled) {
-            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
-        } else {
-            return event.getX(0);
-        }
-    }
-
-    @Override
-    public float getEventY(MotionEvent event) {
-        if (mRelativeModeEnabled) {
-            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
-        } else {
-            return event.getY(0);
-        }
-    }
-}
-
-class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
-    // Generic Motion (mouse hover, joystick...) events go here
-    private boolean mRelativeModeEnabled;
-
-    @Override
-    public boolean onGenericMotion(View v, MotionEvent event) {
-        float x, y;
-        int action;
-
-        switch ( event.getSource() ) {
-            case InputDevice.SOURCE_JOYSTICK:
-                return SDLControllerManager.handleJoystickMotionEvent(event);
-
-            case InputDevice.SOURCE_MOUSE:
-            // DeX desktop mouse cursor is a separate non-standard input type.
-            case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
-                action = event.getActionMasked();
-                switch (action) {
-                    case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    default:
-                        break;
-                }
-                break;
-
-            case InputDevice.SOURCE_MOUSE_RELATIVE:
-                action = event.getActionMasked();
-                switch (action) {
-                    case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
-                        SDLActivity.onNativeMouse(0, action, x, y, true);
-                        return true;
-
-                    default:
-                        break;
-                }
-                break;
-
-            default:
-                break;
-        }
-
-        // Event was not managed
-        return false;
-    }
-
-    @Override
-    public boolean supportsRelativeMouse() {
-        return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27));
-    }
-
-    @Override
-    public boolean inRelativeMode() {
-        return mRelativeModeEnabled;
-    }
-
-    @Override
-    public boolean setRelativeMouseEnabled(boolean enabled) {
-        if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) {
-            if (enabled) {
-                SDLActivity.getContentView().requestPointerCapture();
-            } else {
-                SDLActivity.getContentView().releasePointerCapture();
-            }
-            mRelativeModeEnabled = enabled;
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    @Override
-    public void reclaimRelativeMouseModeIfNeeded()
-    {
-        if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
-            SDLActivity.getContentView().requestPointerCapture();
-        }
-    }
-
-    @Override
-    public float getEventX(MotionEvent event) {
-        // Relative mouse in capture mode will only have relative for X/Y
-        return event.getX(0);
-    }
-
-    @Override
-    public float getEventY(MotionEvent event) {
-        // Relative mouse in capture mode will only have relative for X/Y
-        return event.getY(0);
-    }
-}

+ 0 - 405
android/vcmi-app/src/main/java/org/libsdl/app/SDLSurface.java

@@ -1,405 +0,0 @@
-package org.libsdl.app;
-
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Build;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.WindowManager;
-
-
-/**
-    SDLSurface. This is what we draw on, so we need to know when it's created
-    in order to do anything useful.
-
-    Because of this, that's where we set up the SDL thread
-*/
-public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
-    View.OnKeyListener, View.OnTouchListener, SensorEventListener  {
-
-    // Sensors
-    protected SensorManager mSensorManager;
-    protected Display mDisplay;
-
-    // Keep track of the surface size to normalize touch events
-    protected float mWidth, mHeight;
-
-    // Is SurfaceView ready for rendering
-    public boolean mIsSurfaceReady;
-
-    // Startup
-    public SDLSurface(Context context) {
-        super(context);
-        getHolder().addCallback(this);
-
-        setFocusable(true);
-        setFocusableInTouchMode(true);
-        requestFocus();
-        setOnKeyListener(this);
-        setOnTouchListener(this);
-
-        mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
-        mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
-
-        setOnGenericMotionListener(SDLActivity.getMotionListener());
-
-        // Some arbitrary defaults to avoid a potential division by zero
-        mWidth = 1.0f;
-        mHeight = 1.0f;
-
-        mIsSurfaceReady = false;
-    }
-
-    public void handlePause() {
-        enableSensor(Sensor.TYPE_ACCELEROMETER, false);
-    }
-
-    public void handleResume() {
-        setFocusable(true);
-        setFocusableInTouchMode(true);
-        requestFocus();
-        setOnKeyListener(this);
-        setOnTouchListener(this);
-        enableSensor(Sensor.TYPE_ACCELEROMETER, true);
-    }
-
-    public Surface getNativeSurface() {
-        return getHolder().getSurface();
-    }
-
-    // Called when we have a valid drawing surface
-    @Override
-    public void surfaceCreated(SurfaceHolder holder) {
-        Log.v("SDL", "surfaceCreated()");
-        SDLActivity.onNativeSurfaceCreated();
-    }
-
-    // Called when we lose the surface
-    @Override
-    public void surfaceDestroyed(SurfaceHolder holder) {
-        Log.v("SDL", "surfaceDestroyed()");
-
-        // Transition to pause, if needed
-        SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
-        SDLActivity.handleNativeState();
-
-        mIsSurfaceReady = false;
-        SDLActivity.onNativeSurfaceDestroyed();
-    }
-
-    // Called when the surface is resized
-    @Override
-    public void surfaceChanged(SurfaceHolder holder,
-                               int format, int width, int height) {
-        Log.v("SDL", "surfaceChanged()");
-
-        if (SDLActivity.mSingleton == null) {
-            return;
-        }
-
-        mWidth = width;
-        mHeight = height;
-        int nDeviceWidth = width;
-        int nDeviceHeight = height;
-        try
-        {
-            if (Build.VERSION.SDK_INT >= 17) {
-                DisplayMetrics realMetrics = new DisplayMetrics();
-                mDisplay.getRealMetrics( realMetrics );
-                nDeviceWidth = realMetrics.widthPixels;
-                nDeviceHeight = realMetrics.heightPixels;
-            }
-        } catch(Exception ignored) {
-        }
-
-        synchronized(SDLActivity.getContext()) {
-            // In case we're waiting on a size change after going fullscreen, send a notification.
-            SDLActivity.getContext().notifyAll();
-        }
-
-        Log.v("SDL", "Window size: " + width + "x" + height);
-        Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
-        SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
-        SDLActivity.onNativeResize();
-
-        // Prevent a screen distortion glitch,
-        // for instance when the device is in Landscape and a Portrait App is resumed.
-        boolean skip = false;
-        int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
-
-        if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
-            if (mWidth > mHeight) {
-               skip = true;
-            }
-        } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
-            if (mWidth < mHeight) {
-               skip = true;
-            }
-        }
-
-        // Special Patch for Square Resolution: Black Berry Passport
-        if (skip) {
-           double min = Math.min(mWidth, mHeight);
-           double max = Math.max(mWidth, mHeight);
-
-           if (max / min < 1.20) {
-              Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
-              skip = false;
-           }
-        }
-
-        // Don't skip in MultiWindow.
-        if (skip) {
-            if (Build.VERSION.SDK_INT >= 24) {
-                if (SDLActivity.mSingleton.isInMultiWindowMode()) {
-                    Log.v("SDL", "Don't skip in Multi-Window");
-                    skip = false;
-                }
-            }
-        }
-
-        if (skip) {
-           Log.v("SDL", "Skip .. Surface is not ready.");
-           mIsSurfaceReady = false;
-           return;
-        }
-
-        /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
-        SDLActivity.onNativeSurfaceChanged();
-
-        /* Surface is ready */
-        mIsSurfaceReady = true;
-
-        SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
-        SDLActivity.handleNativeState();
-    }
-
-    // Key events
-    @Override
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        return SDLActivity.handleKeyEvent(v, keyCode, event, null);
-    }
-
-    // Touch events
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        /* Ref: http://developer.android.com/training/gestures/multi.html */
-        int touchDevId = event.getDeviceId();
-        final int pointerCount = event.getPointerCount();
-        int action = event.getActionMasked();
-        int pointerFingerId;
-        int i = -1;
-        float x,y,p;
-
-        /*
-         * Prevent id to be -1, since it's used in SDL internal for synthetic events
-         * Appears when using Android emulator, eg:
-         *  adb shell input mouse tap 100 100
-         *  adb shell input touchscreen tap 100 100
-         */
-        if (touchDevId < 0) {
-            touchDevId -= 1;
-        }
-
-        // 12290 = Samsung DeX mode desktop mouse
-        // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
-        // 0x2   = SOURCE_CLASS_POINTER
-        if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
-            int mouseButton = 1;
-            try {
-                Object object = event.getClass().getMethod("getButtonState").invoke(event);
-                if (object != null) {
-                    mouseButton = (Integer) object;
-                }
-            } catch(Exception ignored) {
-            }
-
-            // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
-            // if we are.  We'll leverage our existing mouse motion listener
-            SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
-            x = motionListener.getEventX(event);
-            y = motionListener.getEventY(event);
-
-            SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
-        } else {
-            switch(action) {
-                case MotionEvent.ACTION_MOVE:
-                    for (i = 0; i < pointerCount; i++) {
-                        pointerFingerId = event.getPointerId(i);
-                        x = event.getX(i) / mWidth;
-                        y = event.getY(i) / mHeight;
-                        p = event.getPressure(i);
-                        if (p > 1.0f) {
-                            // may be larger than 1.0f on some devices
-                            // see the documentation of getPressure(i)
-                            p = 1.0f;
-                        }
-                        SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_DOWN:
-                    // Primary pointer up/down, the index is always zero
-                    i = 0;
-                    /* fallthrough */
-                case MotionEvent.ACTION_POINTER_UP:
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    // Non primary pointer up/down
-                    if (i == -1) {
-                        i = event.getActionIndex();
-                    }
-
-                    pointerFingerId = event.getPointerId(i);
-                    x = event.getX(i) / mWidth;
-                    y = event.getY(i) / mHeight;
-                    p = event.getPressure(i);
-                    if (p > 1.0f) {
-                        // may be larger than 1.0f on some devices
-                        // see the documentation of getPressure(i)
-                        p = 1.0f;
-                    }
-                    SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
-                    break;
-
-                case MotionEvent.ACTION_CANCEL:
-                    for (i = 0; i < pointerCount; i++) {
-                        pointerFingerId = event.getPointerId(i);
-                        x = event.getX(i) / mWidth;
-                        y = event.getY(i) / mHeight;
-                        p = event.getPressure(i);
-                        if (p > 1.0f) {
-                            // may be larger than 1.0f on some devices
-                            // see the documentation of getPressure(i)
-                            p = 1.0f;
-                        }
-                        SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
-                    }
-                    break;
-
-                default:
-                    break;
-            }
-        }
-
-        return true;
-   }
-
-    // Sensor events
-    public void enableSensor(int sensortype, boolean enabled) {
-        // TODO: This uses getDefaultSensor - what if we have >1 accels?
-        if (enabled) {
-            mSensorManager.registerListener(this,
-                            mSensorManager.getDefaultSensor(sensortype),
-                            SensorManager.SENSOR_DELAY_GAME, null);
-        } else {
-            mSensorManager.unregisterListener(this,
-                            mSensorManager.getDefaultSensor(sensortype));
-        }
-    }
-
-    @Override
-    public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        // TODO
-    }
-
-    @Override
-    public void onSensorChanged(SensorEvent event) {
-        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
-
-            // Since we may have an orientation set, we won't receive onConfigurationChanged events.
-            // We thus should check here.
-            int newOrientation;
-
-            float x, y;
-            switch (mDisplay.getRotation()) {
-                case Surface.ROTATION_90:
-                    x = -event.values[1];
-                    y = event.values[0];
-                    newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
-                    break;
-                case Surface.ROTATION_270:
-                    x = event.values[1];
-                    y = -event.values[0];
-                    newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
-                    break;
-                case Surface.ROTATION_180:
-                    x = -event.values[0];
-                    y = -event.values[1];
-                    newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
-                    break;
-                case Surface.ROTATION_0:
-                default:
-                    x = event.values[0];
-                    y = event.values[1];
-                    newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
-                    break;
-            }
-
-            if (newOrientation != SDLActivity.mCurrentOrientation) {
-                SDLActivity.mCurrentOrientation = newOrientation;
-                SDLActivity.onNativeOrientationChanged(newOrientation);
-            }
-
-            SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
-                                      y / SensorManager.GRAVITY_EARTH,
-                                      event.values[2] / SensorManager.GRAVITY_EARTH);
-
-
-        }
-    }
-
-    // Captured pointer events for API 26.
-    public boolean onCapturedPointerEvent(MotionEvent event)
-    {
-        int action = event.getActionMasked();
-
-        float x, y;
-        switch (action) {
-            case MotionEvent.ACTION_SCROLL:
-                x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                SDLActivity.onNativeMouse(0, action, x, y, false);
-                return true;
-
-            case MotionEvent.ACTION_HOVER_MOVE:
-            case MotionEvent.ACTION_MOVE:
-                x = event.getX(0);
-                y = event.getY(0);
-                SDLActivity.onNativeMouse(0, action, x, y, true);
-                return true;
-
-            case MotionEvent.ACTION_BUTTON_PRESS:
-            case MotionEvent.ACTION_BUTTON_RELEASE:
-
-                // Change our action value to what SDL's code expects.
-                if (action == MotionEvent.ACTION_BUTTON_PRESS) {
-                    action = MotionEvent.ACTION_DOWN;
-                } else { /* MotionEvent.ACTION_BUTTON_RELEASE */
-                    action = MotionEvent.ACTION_UP;
-                }
-
-                x = event.getX(0);
-                y = event.getY(0);
-                int button = event.getButtonState();
-
-                SDLActivity.onNativeMouse(button, action, x, y, true);
-                return true;
-        }
-
-        return false;
-    }
-}

+ 3 - 3
client/CMakeLists.txt

@@ -437,7 +437,7 @@ set(vcmiclientcommon_HEADERS
 	UIHelper.h
 )
 
-if(APPLE_IOS)
+if(IOS)
 	set(vcmiclientcommon_SRCS ${vcmiclientcommon_SRCS}
 		ios/utils.mm
 	)
@@ -461,7 +461,7 @@ if(NOT ENABLE_STATIC_LIBS)
 		add_dependencies(vcmiclientcommon Nullkiller2)
 	endif()
 endif()
-if(APPLE_IOS)
+if(IOS)
 	if(ENABLE_ERM)
 		add_dependencies(vcmiclientcommon vcmiERM)
 	endif()
@@ -481,7 +481,7 @@ if(WIN32)
 		target_link_libraries(vcmiclientcommon SDL2::SDL2main)
 	endif()
 	target_compile_definitions(vcmiclientcommon PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
-elseif(APPLE_IOS)
+elseif(IOS)
 	target_link_libraries(vcmiclientcommon PRIVATE
 		iOS_utils
 

+ 3 - 2
client/GameEngine.cpp

@@ -281,10 +281,11 @@ void GameEngine::setStatusbar(const std::shared_ptr<IStatusBar> & newStatusBar)
 	currentStatusBar = newStatusBar;
 }
 
-void GameEngine::onScreenResize(bool resolutionChanged)
+void GameEngine::onScreenResize(bool resolutionChanged, bool windowResized)
 {
 	if(resolutionChanged)
-		screenHandler().onScreenResize();
+		if(!screenHandler().onScreenResize(windowResized))
+			return;
 
 	windows().onScreenResize();
 	ENGINE->cursor().onScreenResize();

+ 1 - 1
client/GameEngine.h

@@ -122,7 +122,7 @@ public:
 	[[noreturn]] void mainLoop();
 
 	/// called whenever SDL_WINDOWEVENT_RESTORED is reported or the user selects a different resolution, requiring to center/resize all windows
-	void onScreenResize(bool resolutionChanged);
+	void onScreenResize(bool resolutionChanged, bool windowResized);
 
 	/// Simulate mouse movement to force refresh UI state that updates on mouse move
 	void fakeMouseMove();

+ 2 - 2
client/adventureMap/AdventureMapShortcuts.cpp

@@ -572,9 +572,9 @@ bool AdventureMapShortcuts::optionCanToggleLevel()
 	return optionSidePanelActive() && GAME->interface()->cb->getMapSize().z > 1;
 }
 
-bool AdventureMapShortcuts::optionMapLevelSurface()
+int AdventureMapShortcuts::optionMapLevel()
 {
-	return mapLevel == 0;
+	return mapLevel;
 }
 
 bool AdventureMapShortcuts::optionHeroSleeping()

+ 1 - 1
client/adventureMap/AdventureMapShortcuts.h

@@ -84,7 +84,7 @@ public:
 
 	bool optionCanViewQuests();
 	bool optionCanToggleLevel();
-	bool optionMapLevelSurface();
+	int optionMapLevel();
 	bool optionHeroSleeping();
 	bool optionHeroAwake();
 	bool optionHeroSelected();

+ 10 - 3
client/adventureMap/AdventureMapWidget.cpp

@@ -30,7 +30,9 @@
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 
+#include "../../lib/callback/CCallback.h"
 #include "../../lib/constants/StringConstants.h"
+#include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/filesystem/ResourcePath.h"
 
 AdventureMapWidget::AdventureMapWidget( std::shared_ptr<AdventureMapShortcuts> shortcuts )
@@ -402,6 +404,8 @@ void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget)
 	{
 		auto container = dynamic_cast<CAdventureMapContainerWidget *>(entry);
 
+		int mapLevels = GAME->interface()->cb->getMapHeader()->mapLevels;
+
 		if (container)
 		{
 			if (container->disableCondition == "heroAwake")
@@ -410,11 +414,14 @@ void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget)
 			if (container->disableCondition == "heroSleeping")
 				container->setEnabled(shortcuts->optionHeroSleeping());
 
-			if (container->disableCondition == "mapLayerSurface") // TODO: multilevel support
-				container->setEnabled(shortcuts->optionMapLevelSurface());
+			if (container->disableCondition == "mapLayerSurface")
+				container->setEnabled(shortcuts->optionMapLevel() == 0);
 
 			if (container->disableCondition == "mapLayerUnderground")
-				container->setEnabled(!shortcuts->optionMapLevelSurface());
+				container->setEnabled(shortcuts->optionMapLevel() == mapLevels - 1);
+
+			if (container->disableCondition == "mapLayerOther")
+				container->setEnabled(shortcuts->optionMapLevel() > 0 && shortcuts->optionMapLevel() < mapLevels - 1);
 
 			if (container->disableCondition == "mapViewMode")
 				container->setEnabled(shortcuts->optionInWorldView());

+ 2 - 4
client/eventsSDL/InputHandler.cpp

@@ -248,17 +248,15 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 #ifndef VCMI_IOS
 			{
 				std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-				ENGINE->onScreenResize(false);
+				ENGINE->onScreenResize(false, false);
 			}
 #endif
 				break;
 			case SDL_WINDOWEVENT_SIZE_CHANGED:
-#ifdef VCMI_MOBILE
 			{
 				std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-				ENGINE->onScreenResize(true);
+				ENGINE->onScreenResize(true, true);
 			}
-#endif
 				break;
 			case SDL_WINDOWEVENT_FOCUS_GAINED:
 			{

+ 1 - 1
client/eventsSDL/InputSourceKeyboard.cpp

@@ -97,7 +97,7 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 	{
 		Settings full = settings.write["video"]["fullscreen"];
 		full->Bool() = !full->Bool();
-		ENGINE->onScreenResize(true);
+		ENGINE->onScreenResize(true, false);
 	}
 
 	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_TRACK_HERO))

+ 1 - 1
client/media/CMusicHandler.cpp

@@ -269,7 +269,7 @@ void MusicEntry::load(const AudioPath & musicURI)
 		auto * musicFile = MakeSDLRWops(std::move(stream));
 		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
 	}
-	catch(std::exception & e)
+	catch(const std::exception & e)
 	{
 		logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
 		logGlobal->error("Exception: %s", e.what());

+ 105 - 0
client/render/AssetGenerator.cpp

@@ -56,6 +56,8 @@ void AssetGenerator::initialize()
 
 	for(int i = 1; i < 9; i++)
 		imageFiles[ImagePath::builtin("CampaignHc" + std::to_string(i) + "Image.png")] = [this, i](){ return createChroniclesCampaignImages(i);};
+	
+	animationFiles[AnimationPath::builtin("SPRITES/adventureLayersButton")] = createAdventureMapButton(ImagePath::builtin("adventureLayers.png"));
 
 	createPaletteShiftedSprites();
 }
@@ -428,5 +430,108 @@ AssetGenerator::CanvasPtr AssetGenerator::createPaletteShiftedImage(const Animat
 	canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2));
 
 	return image;
+}
+
+void meanImage(AssetGenerator::CanvasPtr dst, std::vector<Canvas> & images)
+{
+	auto image = dst->getCanvas();
+
+	for(int x = 0; x < dst->width(); x++)
+		for(int y = 0; y < dst->height(); y++)
+		{
+			int sumR = 0;
+			int sumG = 0;
+			int sumB = 0;
+			int sumA = 0;
+			for(auto & img : images)
+			{
+				auto color = img.getPixel(Point(x, y));
+				sumR += color.r;
+				sumG += color.g;
+				sumB += color.b;
+				sumA += color.a;
+			}
+			int ct = images.size();
+			image.drawPoint(Point(x, y), ColorRGBA(sumR / ct, sumG / ct, sumB / ct, sumA / ct));
+		}
+}
+
+AssetGenerator::CanvasPtr AssetGenerator::createAdventureMapButtonClear(const PlayerColor & player) const
+{
+	auto imageNames = { "iam002", "iam003", "iam004", "iam005", "iam006", "iam007", "iam008", "iam009", "iam010", "iam011" };
+	std::vector<Canvas> images;
+
+	CanvasPtr dst = nullptr;
+	for(auto & imageName : imageNames)
+	{
+		auto animation = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin(imageName), EImageBlitMode::COLORKEY);
+		animation->playerColored(player);
+		auto image = ENGINE->renderHandler().createImage(animation->getImage(2)->dimensions(), CanvasScalingPolicy::IGNORE);
+		if(!dst)
+			dst = ENGINE->renderHandler().createImage(animation->getImage(2)->dimensions(), CanvasScalingPolicy::IGNORE);
+		Canvas canvas = image->getCanvas();	
+		canvas.draw(animation->getImage(2), Point(0, 0));
+		images.push_back(image->getCanvas());
+	}
+
+	meanImage(dst, images);
+
+	return dst;
+}
+
+AssetGenerator::AnimationLayoutMap AssetGenerator::createAdventureMapButton(const ImagePath & overlay)
+{
+	std::shared_ptr<IImage> overlayImg = ENGINE->renderHandler().loadImage(ImageLocator(overlay, EImageBlitMode::OPAQUE));
+	auto overlayCanvasImg = ENGINE->renderHandler().createImage(overlayImg->dimensions(), CanvasScalingPolicy::IGNORE);
+	Canvas overlayCanvas = overlayCanvasImg->getCanvas();
+	overlayCanvas.draw(overlayImg, Point(0, 0));
+
+	AnimationLayoutMap layout;
+	for (PlayerColor color(0); color < PlayerColor::PLAYER_LIMIT; ++color)
+	{
+		auto clearButtonImg = createAdventureMapButtonClear(color);
+		for(int i = 0; i < 4; i++)
+		{
+			ImagePath spriteName = ImagePath::builtin(overlay.getOriginalName() + "Btn" + std::to_string(i) + ".png");
+			ImagePath spriteNameColor = ImagePath::builtin(overlay.getOriginalName() + "Btn" + std::to_string(i) + "-" + color.toString() + ".png");
+
+			imageFiles[spriteNameColor] = [overlayCanvasImg, clearButtonImg, i](){
+				auto newImg = ENGINE->renderHandler().createImage(overlayCanvasImg->dimensions(), CanvasScalingPolicy::IGNORE);
+				auto canvas = newImg->getCanvas();
+				canvas.draw(clearButtonImg, Point(0, 0));
+				switch (i)
+				{
+				case 0:
+					canvas.draw(overlayCanvasImg, Point(0, 0));
+					return newImg;
+				case 1:
+					canvas.draw(clearButtonImg, Point(1, 1));
+					canvas.draw(overlayCanvasImg, Point(1, 1));
+					canvas.drawLine(Point(0, 0), Point(newImg->width() - 1, 0), ColorRGBA(0, 0, 0), ColorRGBA(0, 0, 0));
+					canvas.drawLine(Point(0, 0), Point(0, newImg->height() - 1), ColorRGBA(0, 0, 0), ColorRGBA(0, 0, 0));
+					canvas.drawColorBlended(Rect(0, 0, newImg->width(), 4), ColorRGBA(0, 0, 0, 160));
+					canvas.drawColorBlended(Rect(0, 0, 4, newImg->height()), ColorRGBA(0, 0, 0, 160));
+					return newImg;
+				case 2:
+					canvas.drawTransparent(overlayCanvasImg->getCanvas(), Point(0, 0), 0.25);
+					return newImg;
+				default:
+					canvas.draw(overlayCanvasImg, Point(0, 0));
+					canvas.drawLine(Point(0, 0), Point(newImg->width() - 1, 0), ColorRGBA(255, 255, 255), ColorRGBA(255, 255, 255));
+					canvas.drawLine(Point(newImg->width() - 1, 0), Point(newImg->width() - 1, newImg->height() - 1), ColorRGBA(255, 255, 255), ColorRGBA(255, 255, 255));
+					canvas.drawLine(Point(newImg->width() - 1, newImg->height() - 1), Point(0, newImg->height() - 1), ColorRGBA(255, 255, 255), ColorRGBA(255, 255, 255));
+					canvas.drawLine(Point(0, newImg->height() - 1), Point(0, 0), ColorRGBA(255, 255, 255), ColorRGBA(255, 255, 255));
+					return newImg;
+				}
+			};
+
+			if(color == PlayerColor(0))
+			{
+				layout[0].push_back(ImageLocator(spriteName, EImageBlitMode::SIMPLE));
+				imageFiles[spriteName] = imageFiles[spriteNameColor];
+			}
+		}
+	}
 
+	return layout;
 }

+ 2 - 0
client/render/AssetGenerator.h

@@ -53,6 +53,8 @@ private:
 	CanvasPtr createSpellTabNone() const;
 	CanvasPtr createChroniclesCampaignImages(int chronicle) const;
 	CanvasPtr createPaletteShiftedImage(const AnimationPath & source, const std::vector<PaletteAnimation> & animation, int frameIndex, int paletteShiftCounter) const;
+	CanvasPtr createAdventureMapButtonClear(const PlayerColor & player) const;
+	AnimationLayoutMap createAdventureMapButton(const ImagePath & overlay);
 
 	void createPaletteShiftedSprites();
 	void generatePaletteShiftedAnimation(const AnimationPath & source, const std::vector<PaletteAnimation> & animation);

+ 7 - 0
client/render/Canvas.cpp

@@ -238,6 +238,13 @@ Rect Canvas::getRenderArea() const
 	return renderArea;
 }
 
+ColorRGBA Canvas::getPixel(const Point & position) const
+{
+	SDL_Color color;
+	SDL_GetRGBA(CSDL_Ext::getPixel(surface, position.x, position.y), surface->format, &color.r, &color.g, &color.b, &color.a);
+	return ColorRGBA(color.r, color.g, color.b, color.a);
+}
+
 CanvasClipRectGuard::CanvasClipRectGuard(Canvas & canvas, const Rect & rect): surf(canvas.surface)
 {
 	CSDL_Ext::getClipRect(surf, oldRect);

+ 3 - 0
client/render/Canvas.h

@@ -122,6 +122,9 @@ public:
 
 	/// get the render area
 	Rect getRenderArea() const;
+
+	/// get pixel color
+	ColorRGBA getPixel(const Point & position) const;
 };
 
 class CanvasClipRectGuard : boost::noncopyable

+ 4 - 0
client/render/IImage.h

@@ -128,6 +128,10 @@ public:
 	/// Returns true if this image is still loading and can't be used
 	virtual bool isLoading() const = 0;
 
+	/// When disabled upscaling needs to be done in sync (e.g. because there is no 1x base image)
+	virtual void setAsyncUpscale(bool on) = 0;
+	virtual bool getAsyncUpscale() const = 0;
+
 	virtual ~ISharedImage() = default;
 
 	virtual const SDL_Palette * getPalette() const = 0;

+ 1 - 1
client/render/IScreenHandler.h

@@ -25,7 +25,7 @@ public:
 	virtual ~IScreenHandler() = default;
 
 	/// Updates window state after fullscreen state has been changed in settings
-	virtual void onScreenResize() = 0;
+	virtual bool onScreenResize(bool keepWindowResolution) = 0;
 
 	/// Fills screen with black color, erasing any existing content
 	virtual void clearScreen() = 0;

+ 5 - 0
client/renderSDL/RenderHandler.cpp

@@ -322,6 +322,8 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 		img = std::make_shared<SDLImageShared>(imagePathData, optimizeImage);
 	else if(CResourceHandler::get()->existsResource(imagePath))
 		img = std::make_shared<SDLImageShared>(imagePath, optimizeImage);
+	else if(locator.scalingFactor == 1)
+		img = std::dynamic_pointer_cast<SDLImageShared>(assetGenerator->generateImage(imagePath));
 
 	if(img)
 	{
@@ -331,6 +333,9 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 			img = img->drawShadow((*locator.generateShadow) == SharedImageLocator::ShadowMode::SHADOW_SHEAR);
 		if(isOverlay && generateOverlay && (*locator.generateOverlay) == SharedImageLocator::OverlayMode::OVERLAY_OUTLINE)
 			img = img->drawOutline(Colors::WHITE, 1);
+
+		if(locator.scalingFactor == 1)
+			img->setAsyncUpscale(false); // no base image, needs to be done in sync
 	}
 
 	return img;

+ 11 - 1
client/renderSDL/SDLImage.cpp

@@ -271,7 +271,7 @@ std::shared_ptr<SDLImageShared> SDLImageShared::createScaled(const SDLImageShare
 		self->upscalingInProgress = false;
 	};
 
-	if(settings["video"]["asyncUpscaling"].Bool())
+	if(settings["video"]["asyncUpscaling"].Bool() && from->getAsyncUpscale())
 		ENGINE->async().run(scalingTask);
 	else
 		scalingTask();
@@ -284,6 +284,16 @@ bool SDLImageShared::isLoading() const
 	return upscalingInProgress;
 }
 
+void SDLImageShared::setAsyncUpscale(bool on)
+{
+	asyncUpscale = on;
+}
+
+bool SDLImageShared::getAsyncUpscale() const
+{
+	return asyncUpscale;
+}
+
 std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
 {
 	if(upscalingInProgress)

+ 3 - 0
client/renderSDL/SDLImage.h

@@ -36,6 +36,7 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 	Point fullSize;
 
 	std::atomic_bool upscalingInProgress = false;
+	bool asyncUpscale = true;
 
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
@@ -63,6 +64,8 @@ public:
 	Rect contentRect() const override;
 
 	bool isLoading() const override;
+	void setAsyncUpscale(bool on) override;
+	bool getAsyncUpscale() const override;
 
 	const SDL_Palette * getPalette() const override;
 

+ 16 - 2
client/renderSDL/ScreenHandler.cpp

@@ -298,7 +298,6 @@ void ScreenHandler::updateWindowState()
 			Point resolution = getPreferredWindowResolution();
 			SDL_SetWindowFullscreen(mainWindow, 0);
 			SDL_SetWindowSize(mainWindow, resolution.x, resolution.y);
-			SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex));
 			return;
 		}
 	}
@@ -480,9 +479,24 @@ SDL_Window * ScreenHandler::createWindow()
 #endif
 }
 
-void ScreenHandler::onScreenResize()
+bool ScreenHandler::onScreenResize(bool keepWindowResolution)
 {
+	if(getPreferredWindowMode() == EWindowMode::WINDOWED && keepWindowResolution)
+	{
+		auto res = getRenderResolution();
+
+		if(res.x < heroes3Resolution.x || res.y < heroes3Resolution.y)
+			return false;
+
+		Settings video = settings.write["video"];
+		video["resolution"]["width"].Integer() = res.x;
+		video["resolution"]["height"].Integer() = res.y;
+	}
+	else if(keepWindowResolution)
+		return false;
+
 	recreateWindowAndScreenBuffers();
+	return true;
 }
 
 void ScreenHandler::validateSettings()

+ 1 - 1
client/renderSDL/ScreenHandler.h

@@ -100,7 +100,7 @@ public:
 	~ScreenHandler();
 
 	/// Updates and potentially recreates target screen to match selected fullscreen status
-	void onScreenResize() final;
+	bool onScreenResize(bool keepWindowResolution) final;
 
 	/// Fills screen with black color, erasing any existing content
 	void clearScreen() final;

+ 4 - 4
client/windows/CCreatureWindow.cpp

@@ -868,10 +868,10 @@ void CStackWindow::initBonusesList()
 			return  info->stackNode->bonusToString(v1) < info->stackNode->bonusToString(v2);
 	};
 
-	// these bonuses require special handling. For example they come with own descriptions, for use in morale/luck description
-	// also, this information is already available in creature window
-	receivedBonuses.remove_if(Selector::type()(BonusType::MORALE));
-	receivedBonuses.remove_if(Selector::type()(BonusType::LUCK));
+	receivedBonuses.remove_if([](const Bonus* b)
+	{
+		return !LIBRARY->bth->shouldPropagateDescription(b->type);
+	});
 
 	std::vector<BonusList> groupedBonuses;
 	while (!receivedBonuses.empty())

+ 3 - 3
client/windows/settings/GeneralOptionsTab.cpp

@@ -336,7 +336,7 @@ void GeneralOptionsTab::setGameResolution(int index)
 	widget<CLabel>("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y));
 
 	ENGINE->dispatchMainThread([](){
-		ENGINE->onScreenResize(true);
+		ENGINE->onScreenResize(true, false);
 	});
 }
 
@@ -360,7 +360,7 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive)
 	updateResolutionSelector();
 
 	ENGINE->dispatchMainThread([](){
-		ENGINE->onScreenResize(true);
+		ENGINE->onScreenResize(true, false);
 	});
 }
 
@@ -419,7 +419,7 @@ void GeneralOptionsTab::setGameScaling(int index)
 	widget<CLabel>("scalingLabel")->setText(scalingToLabelString(scaling));
 
 	ENGINE->dispatchMainThread([](){
-		ENGINE->onScreenResize(true);
+		ENGINE->onScreenResize(true, false);
 	});
 }
 

+ 6 - 16
clientapp/CMakeLists.txt

@@ -7,7 +7,7 @@ set(clientapp_HEADERS
 		StdInc.h
 )
 
-if(APPLE_IOS)
+if(IOS)
 	set(clientapp_SRCS ${clientapp_SRCS}
 		CFocusableHelper.cpp
 		ios/GameChatKeyboardHandler.m
@@ -30,6 +30,7 @@ if(ANDROID)
 	)
 else()
 	add_executable(vcmiclient ${clientapp_SRCS} ${clientapp_HEADERS})
+	vcmi_create_exe_shim(vcmiclient)
 endif()
 
 target_link_libraries(vcmiclient PRIVATE vcmiclientcommon)
@@ -55,16 +56,7 @@ if(WIN32)
 		target_link_libraries(vcmiclient SDL2::SDL2main)
 	endif()
 	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
-
-	# TODO: very hacky, find proper solution to copy AI dlls into bin dir
-	if(MSVC)
-		add_custom_command(TARGET vcmiclient POST_BUILD
-			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
-			COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
-			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
-		)
-	endif()
-elseif(APPLE_IOS)
+elseif(IOS)
 	set_target_properties(vcmiclient PROPERTIES
 		MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
 		XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
@@ -89,8 +81,7 @@ endif()
 vcmi_set_output_dir(vcmiclient "")
 enable_pch(vcmiclient)
 
-if(APPLE_IOS)
-	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
+if(IOS)
 	add_custom_command(TARGET vcmiclient POST_BUILD
 		COMMAND ios/set_build_version.sh "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
 		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
@@ -103,11 +94,10 @@ elseif(ANDROID)
 	find_program(androidDeployQt androiddeployqt
 		PATHS "${qtBinDir}"
 	)
-	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
-
 	add_custom_target(android_deploy ALL
 		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
-		COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
+		COMMAND ${CMAKE_COMMAND} -E env "ORG_GRADLE_PROJECT_SDL_JAVA_SRC_DIR=${SDL_JAVA_SRC_DIR}" --
+			"${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
 		COMMAND_EXPAND_LISTS
 		VERBATIM
 		COMMENT "Create android package"

+ 3 - 3
cmake_modules/FindSDL2.cmake

@@ -273,7 +273,7 @@ if(SDL2_LIBRARY)
   # I think it has something to do with the CACHE STRING.
   # So I use a temporary variable until the end so I can set the
   # "real" variable in one-shot.
-  if(APPLE_MACOS)
+  if(MACOS)
     set(SDL2_LIBRARIES ${SDL2_LIBRARIES} -framework Cocoa)
   endif()
 
@@ -335,10 +335,10 @@ if(SDL2_FOUND)
     if(APPLE)
       # For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
       # For more details, please see above.
-      if (APPLE_MACOS)
+      if (MACOS)
         set_property(TARGET SDL2::SDL2 APPEND PROPERTY
                      INTERFACE_LINK_OPTIONS -framework Cocoa)
-      elseif (APPLE_IOS)
+      elseif (IOS)
         target_link_libraries(SDL2::SDL2 INTERFACE
           "-framework AudioToolbox"
           "-framework AVFoundation"

+ 1 - 1
cmake_modules/FindSDL2_image.cmake

@@ -227,7 +227,7 @@ if(SDL2_IMAGE_FOUND)
                           IMPORTED_LOCATION "${SDL2_IMAGE_LIBRARY}"
                           INTERFACE_INCLUDE_DIRECTORIES "${SDL2_IMAGE_INCLUDE_DIR}"
                           INTERFACE_LINK_LIBRARIES SDL2::SDL2)
-    if (APPLE_IOS)
+    if (IOS)
       target_link_libraries(SDL2::Image INTERFACE
         "-framework CoreGraphics"
         "-framework Foundation"

+ 1 - 1
cmake_modules/FindSDL2_mixer.cmake

@@ -216,7 +216,7 @@ if(SDL2_MIXER_FOUND)
                           IMPORTED_LOCATION "${SDL2_MIXER_LIBRARY}"
                           INTERFACE_INCLUDE_DIRECTORIES "${SDL2_MIXER_INCLUDE_DIR}"
                           INTERFACE_LINK_LIBRARIES SDL2::SDL2)
-    if (APPLE_IOS)
+    if (IOS)
       target_link_libraries(SDL2::Mixer INTERFACE
         "-framework AudioToolbox"
         "-framework CoreServices"

+ 1 - 1
cmake_modules/Findminizip.cmake

@@ -33,7 +33,7 @@ else()
   set(VC_LIB_PATH_SUFFIX lib/x86)
 endif()
 
-if (NOT WIN32 AND NOT APPLE_IOS)
+if (NOT WIN32 AND NOT IOS)
     find_package(PkgConfig)
     if (PKG_CONFIG_FOUND)
         pkg_check_modules(_MINIZIP minizip)

+ 28 - 18
cmake_modules/VCMIUtils.cmake

@@ -118,25 +118,35 @@ function(vcmi_print_git_commit_hash)
 
 endfunction()
 
-#install imported target on windows
-function(install_vcpkg_imported_tgt tgt)
-	get_target_property(TGT_LIB_LOCATION ${tgt} LOCATION)
-	get_filename_component(TGT_LIB_FOLDER ${TGT_LIB_LOCATION} PATH)
-	get_filename_component(tgt_name ${TGT_LIB_LOCATION} NAME_WE)
-	get_filename_component(TGT_DLL ${TGT_LIB_FOLDER}/../bin/${tgt_name}.dll ABSOLUTE)
-	message("${tgt_name}: ${TGT_DLL}")
-	install(FILES ${TGT_DLL} DESTINATION ${BIN_DIR})
-endfunction(install_vcpkg_imported_tgt)
-
-# install dependencies from Conan, install_dir should contain \${CMAKE_INSTALL_PREFIX}
-function(vcmi_install_conan_deps install_dir)
+# install dependencies from Conan, CONAN_RUNTIME_LIBS_FILE is set in conanfile.py
+function(vcmi_install_conan_deps)
 	if(NOT USING_CONAN)
 		return()
 	endif()
-	install(CODE "
-		execute_process(COMMAND
-			conan imports \"${CMAKE_SOURCE_DIR}\" --install-folder \"${CONAN_INSTALL_FOLDER}\" --import-folder \"${install_dir}\"
-		)
-		file(REMOVE \"${install_dir}/conan_imports_manifest.txt\")
-	")
+
+	file(STRINGS "${CONAN_RUNTIME_LIBS_FILE}" runtimeLibs)
+	install(FILES ${runtimeLibs} DESTINATION ${LIB_DIR})
+endfunction()
+
+function(vcmi_deploy_qt deployQtToolName deployQtOptions)
+	# TODO: use qt_generate_deploy_app_script() with Qt 6
+	find_program(TOOL_DEPLOYQT NAMES ${deployQtToolName} PATHS "${qtBinDir}")
+	if(TOOL_DEPLOYQT)
+		install(CODE "
+			execute_process(COMMAND \"${TOOL_DEPLOYQT}\" ${deployQtOptions} -verbose=2)
+		")
+	else()
+		message(WARNING "${deployQtToolName} not found, running cpack would result in broken package")
+	endif()
+endfunction()
+
+# generate .bat for .exe with proper PATH
+function(vcmi_create_exe_shim tgt)
+	if(NOT CONAN_RUNENV_SCRIPT)
+		return()
+	endif()
+	file(GENERATE OUTPUT "$<TARGET_FILE_DIR:${tgt}>/$<TARGET_FILE_BASE_NAME:${tgt}>.bat" CONTENT
+"call ${CONAN_RUNENV_SCRIPT}
+@start $<TARGET_FILE_NAME:${tgt}>"
+	)
 endfunction()

+ 47 - 344
conanfile.py

@@ -1,351 +1,54 @@
-from conan import ConanFile
-from conan.errors import ConanInvalidConfiguration
-from conan.tools.apple import is_apple_os
-from conan.tools.build import cross_building
-from conan.tools.cmake import CMakeDeps, CMakeToolchain
-from conans import tools
-
-required_conan_version = ">=1.51.3"
-
-class VCMI(ConanFile):
-    settings = "os", "compiler", "build_type", "arch"
-
-    _libRequires = [
-        "boost/[^1.69]",
-        "minizip/[~1.2.12]",
-    ]
-    _clientRequires = [
-        # Versions between 2.5-2.8 have broken loading of palette sdl images which a lot of mods use
-        # there is workaround that require disabling cmake flag which is not available in conan recipes. 
-        # Bug is fixed in version 2.8, however it is not available in conan at the moment
-        "sdl_image/2.0.5", 
-        "sdl_ttf/[>=2.0.18]",
-        "onetbb/[^2021.7 <2021.10]",  # 2021.10+ breaks mobile builds due to added hwloc dependency
-        "xz_utils/[>=5.2.5]", # Required for innoextract
-    ]
-
-    requires = _libRequires + _clientRequires
-
-    options = {
-        "default_options_of_requirements": [True, False],
-        "with_apple_system_libs": [True, False],
-        "with_ffmpeg": [True, False],
-        "with_luajit": [True, False],
-    }
-    default_options = {
-        "default_options_of_requirements": False,
-        "with_apple_system_libs": False,
-        "with_ffmpeg": True,
-        "with_luajit": False,
-
-        "boost/*:shared": True,
-        "minizip/*:shared": True,
-    }
-
-    def configure(self):
-        self.options["ffmpeg"].shared = self.settings.os == "Android" # using shared version results in less total project size on Android
-        self.options["freetype"].shared = self.settings.os == "Android"
-
-        # SDL_image and Qt depend on it, in iOS both are static
-        self.options["libpng"].shared = self.settings.os != "iOS"
-        # static Qt for iOS is the only viable option at the moment
-        self.options["qt"].shared = self.settings.os != "iOS"
-
-        # TODO: enable for all platforms
-        if self.settings.os == "Android":
-            self.options["bzip2"].shared = True
-            self.options["libiconv"].shared = True
-            self.options["zlib"].shared = True
-
-        # TODO: enable for all platforms?
-        if self.settings.os == "Windows":
-            self.options["sdl"].shared = True
-            self.options["sdl_image"].shared = True
-            self.options["sdl_mixer"].shared = True
-            self.options["sdl_ttf"].shared = True
-
-        if self.settings.os == "iOS": 
-            # TODO: ios - newer sdl fails to link
-            self.requires("sdl/2.26.1")
-            self.requires("sdl_mixer/2.0.4")
-        elif self.settings.os == "Android":
-            # On Android SDL version must be same as version of Java wrapper for SDL in VCMI source code
-            # Wrapper can be found in following directory: android/vcmi-app/src/main/java/org/libsdl/app
-            self.requires("sdl/2.26.5")
-            self.requires("sdl_mixer/2.0.4")
-        else:
-            # upcoming SDL version 3.0+ is not supported at the moment due to API breakage
-            # SDL versions between 2.22-2.26.1 have broken sound
-            self.requires("sdl/[^2.26 || >=2.0.20 <=2.22.0]")
-            self.requires("sdl_mixer/[>=2.0.4]")
-
-        if self.settings.os == "Android":
-            self.options["qt"].android_sdk = tools.get_env("ANDROID_HOME", default="")
-
-        if self.options.default_options_of_requirements:
-            return
-
-        # we need only the following Boost parts:
-        # date_time filesystem iostreams locale program_options system thread
-        # some other parts are also enabled because they're dependents
-        # see e.g. conan-center-index/recipes/boost/all/dependencies
-        self.options["boost"].without_context = True
-        self.options["boost"].without_contract = True
-        self.options["boost"].without_coroutine = True
-        self.options["boost"].without_fiber = True
-        self.options["boost"].without_graph = True
-        self.options["boost"].without_graph_parallel = True
-        self.options["boost"].without_json = True
-        self.options["boost"].without_log = True
-        self.options["boost"].without_math = True
-        self.options["boost"].without_mpi = True
-        self.options["boost"].without_nowide = True
-        self.options["boost"].without_python = True
-        self.options["boost"].without_serialization = True
-        self.options["boost"].without_stacktrace = True
-        self.options["boost"].without_test = True
-        self.options["boost"].without_timer = True
-        self.options["boost"].without_type_erasure = True
-        self.options["boost"].without_wave = True
-        self.options["boost"].without_url = True
-
-        self.options["ffmpeg"].disable_all_bitstream_filters = True
-        self.options["ffmpeg"].disable_all_decoders = True
-        self.options["ffmpeg"].disable_all_demuxers = True
-        self.options["ffmpeg"].disable_all_encoders = True
-        self.options["ffmpeg"].disable_all_filters = True
-        self.options["ffmpeg"].disable_all_hardware_accelerators = True
-        self.options["ffmpeg"].disable_all_muxers = True
-        self.options["ffmpeg"].disable_all_parsers = True
-        self.options["ffmpeg"].disable_all_protocols = True
-
-        self.options["ffmpeg"].with_asm = False
-        self.options["ffmpeg"].with_bzip2 = False
-        self.options["ffmpeg"].with_freetype = False
-        self.options["ffmpeg"].with_libaom = False
-        self.options["ffmpeg"].with_libdav1d = False
-        self.options["ffmpeg"].with_libiconv = False
-        self.options["ffmpeg"].with_libmp3lame = False
-        self.options["ffmpeg"].with_libsvtav1 = False
-        self.options["ffmpeg"].with_libvpx = False
-        self.options["ffmpeg"].with_libwebp = False
-        self.options["ffmpeg"].with_libx264 = False
-        self.options["ffmpeg"].with_libx265 = False
-        self.options["ffmpeg"].with_lzma = True
-        self.options["ffmpeg"].with_openh264 = False
-        self.options["ffmpeg"].with_openjpeg = False
-        self.options["ffmpeg"].with_opus = False
-        self.options["ffmpeg"].with_programs = False
-        self.options["ffmpeg"].with_sdl = False
-        self.options["ffmpeg"].with_ssl = False
-        self.options["ffmpeg"].with_vorbis = False
-        self.options["ffmpeg"].with_zlib = False
-        if self.settings.os != "Android":
-            self.options["ffmpeg"].with_libfdk_aac = False
-
-        self.options["ffmpeg"].avcodec = True
-        self.options["ffmpeg"].avdevice = False
-        self.options["ffmpeg"].avfilter = False
-        self.options["ffmpeg"].avformat = True
-        self.options["ffmpeg"].postproc = False
-        self.options["ffmpeg"].swresample = True # For resampling of audio in 'planar' formats
-        self.options["ffmpeg"].swscale = True    # For video scaling
-
-        # We want following options supported:
-        # H3:SoD - .bik and .smk
-        # H3:HD  -  ogg container / theora video / vorbis sound (not supported by vcmi at the moment, but might be supported in future)
-        # and for mods - webm container / vp8 or vp9 video / opus sound
-        # TODO: add av1 support for mods (requires enabling libdav1d which currently fails to build via Conan)
-        self.options["ffmpeg"].enable_protocols = "file"
-        self.options["ffmpeg"].enable_demuxers = "bink,binka,ogg,smacker,webm_dash_manifest"
-        self.options["ffmpeg"].enable_parsers = "opus,vorbis,vp8,vp9,webp"
-        self.options["ffmpeg"].enable_decoders = "bink,binkaudio_dct,binkaudio_rdft,smackaud,smacker,theora,vorbis,vp8,vp9,opus"
-
-        #optionally, for testing - enable ffplay/ffprobe binaries in conan package:
-        #if self.settings.os == "Windows":
-        #    self.options["ffmpeg"].with_programs = True
-        #    self.options["ffmpeg"].avfilter = True
-        #    self.options["ffmpeg"].with_sdl = True
-        #    self.options["ffmpeg"].enable_filters = "aresample,scale"
-
-        self.options["sdl"].sdl2main = self.settings.os != "iOS"
-        self.options["sdl"].vulkan = False
-
-        # bmp, png are the only ones that needs to be supported
-        # dds support may be useful for HD edition, but not supported by sdl_image at the moment
-        self.options["sdl_image"].gif = False
-        self.options["sdl_image"].lbm = False
-        self.options["sdl_image"].pnm = False
-        self.options["sdl_image"].pcx = False
-        #self.options["sdl_image"].qoi = False # sdl_image >=2.6
-        self.options["sdl_image"].svg = False
-        self.options["sdl_image"].tga = False
-        self.options["sdl_image"].with_libjpeg = False
-        self.options["sdl_image"].with_libtiff = False
-        self.options["sdl_image"].with_libwebp = False
-        self.options["sdl_image"].xcf = False
-        self.options["sdl_image"].xpm = False
-        self.options["sdl_image"].xv = False
-        if is_apple_os(self):
-            self.options["sdl_image"].imageio = True
-
-        # mp3, ogg and wav are the only ones that needs to be supported
-        # opus is nice to have, but fails to build in CI
-        # flac can be considered, but generally unnecessary
-        self.options["sdl_mixer"].flac = False
-        self.options["sdl_mixer"].modplug = False
-        self.options["sdl_mixer"].opus = False
-        if self.settings.os == "iOS" or self.settings.os == "Android":
-            # only available in older sdl_mixer version, removed in newer version
-            self.options["sdl_mixer"].mad = False
-            self.options["sdl_mixer"].mikmod = False
-            self.options["sdl_mixer"].nativemidi = False
-
-        def _disableQtOptions(disableFlag, options):
-            return " ".join([f"-{disableFlag}-{tool}" for tool in options])
-
-        _qtOptions = [
-            _disableQtOptions("no", [
-                "gif",
-                "ico",
-            ]),
-            _disableQtOptions("no-feature", [
-                # xpm format is required for Drag'n'Drop support
-                "imageformat_bmp",
-                "imageformat_jpeg",
-                "imageformat_ppm",
-                "imageformat_xbm",
-
-                # we need only macdeployqt
-                # TODO: disabling these doesn't disable generation of CMake targets
-                # TODO: in Qt 6.3 it's a part of qtbase
-                # "assistant",
-                # "designer",
-                # "distancefieldgenerator",
-                # "kmap2qmap",
-                # "linguist",
-                # "makeqpf",
-                # "pixeltool",
-                # "qdbus",
-                # "qev",
-                # "qtattributionsscanner",
-                # "qtdiag",
-                # "qtpaths",
-                # "qtplugininfo",
-            ]),
-        ]
-        self.options["qt"].config = " ".join(_qtOptions)
-        self.options["qt"].qttools = True
-        self.options["qt"].qtandroidextras = self.settings.os == "Android" # TODO: in Qt 6 it's part of Core
-        self.options["qt"].with_freetype = self.settings.os == "Android"
-        self.options["qt"].with_libjpeg = False
-        self.options["qt"].with_md4c = False
-        self.options["qt"].with_mysql = False
-        self.options["qt"].with_odbc = False
-        self.options["qt"].with_openal = False
-        self.options["qt"].with_pq = False
-        self.options["qt"].openssl = not is_apple_os(self)
-        if self.settings.os == "iOS" or self.settings.os == "Android":
-            self.options["qt"].opengl = "es2"
-        if not is_apple_os(self) and self.settings.os != "Android" and cross_building(self):
-            self.options["qt"].cross_compile = self.env["CONAN_CROSS_COMPILE"]
-        # TODO: add for all platforms after updating recipe
-        if self.settings.os == "Android":
-            self.options["qt"].essential_modules = False
-        # No Qt OpenGL for cross-compiling for Windows, Conan does not support it
-        if self.settings.os == "Windows" and cross_building(self):
-            self.options["qt"].opengl = "no"
-
-        # transitive deps
-        # doesn't link to overridden bzip2 & zlib, the tool isn't needed anyway
-        self.options["pcre2"].build_pcre2grep = False
-        # executable not needed
-        if self.settings.os == "Android":
-            self.options["sqlite3"].build_executable = False
-
-    def requirements(self):
-        self.requires("freetype/[~2.12.1]", override=True) # sdl_ttf / Qt
-        self.requires("libpng/[~1.6.39]", override=True) # freetype / qt / sdl_image
-
-        # client
-        if self.options.with_ffmpeg:
-            self.requires("ffmpeg/[>=4.4]")
-
-        # launcher
-        if self.settings.os == "Android":
-            self.requires("qt/[~5.15.14]")
-        else:
-            self.requires("qt/[~5.15.2]")
-        # TODO: version range doesn't work in Conan v1
-        if self.options["qt"].openssl:
-            self.requires("openssl/1.1.1s")
-
-        # use Apple system libraries instead of external ones
-        if self.options.with_apple_system_libs and is_apple_os(self):
-            systemLibsOverrides = [
-                "bzip2/1.0.8",
-                "libiconv/1.17",
-                "sqlite3/3.39.2",
-                "zlib/1.2.12",
-            ]
-            for lib in systemLibsOverrides:
-                self.requires(f"{lib}@vcmi/apple", override=True)
-        elif self.settings.os == "Android":
-            self.requires("zlib/1.2.12@vcmi/android", override=True)
-        else:
-            self.requires("zlib/[~1.2.13]", override=True) # minizip / Qt
-            self.requires("libiconv/[~1.17]", override=True) # ffmpeg / sdl
-
-        # TODO: the latest official release of LuaJIT (which is quite old) can't be built for arm
-        if self.options.with_luajit and not str(self.settings.arch).startswith("arm"):
-            self.requires("luajit/[~2.0.5]")
-
-    def validate(self):
-        if self.options.with_apple_system_libs and not is_apple_os(self):
-            raise ConanInvalidConfiguration("with_apple_system_libs is only for Apple platforms")
-        if self.options.with_apple_system_libs and self.options.default_options_of_requirements:
-            raise ConanInvalidConfiguration("with_apple_system_libs and default_options_of_requirements can't be True at the same time")
+from dependencies.conanfile import VCMI
+
+from conan.tools.cmake import CMakeToolchain
+from conan.tools.files import save
+
+from glob import glob
+import os
+
+class VCMIApp(VCMI):
+    generators = "CMakeDeps"
+
+    def _pathForCmake(self, path: str) -> str:
+        # CMake doesn't like \ in strings
+        return path.replace(os.path.sep, os.path.altsep) if os.path.altsep else path
+
+    def _generateRuntimeLibsFile(self) -> str:
+        # create file with list of libs to copy to the package for distribution
+        runtimeLibsFile = self._pathForCmake(os.path.join(self.build_folder, "_runtime_libs.txt"))
+
+        runtimeLibExtension = {
+            "Android": "so",
+            "iOS":     "dylib",
+            "Macos":   "dylib",
+            "Windows": "dll",
+        }.get(str(self.settings.os))
+
+        runtimeLibs = []
+        for _, dep in self.dependencies.host.items():
+            # Qt libs are copied using *deployqt
+            if dep.ref.name == "qt":
+                continue
+
+            runtimeLibDir = ''
+            if self.settings.os == "Windows":
+                if len(dep.cpp_info.bindirs) > 0:
+                    runtimeLibDir = dep.cpp_info.bindir
+            elif len(dep.cpp_info.libdirs) > 0:
+                runtimeLibDir = dep.cpp_info.libdir
+            if len(runtimeLibDir) > 0:
+                runtimeLibs += map(self._pathForCmake, glob(os.path.join(runtimeLibDir, f"*.{runtimeLibExtension}")))
+        save(self, runtimeLibsFile, "\n".join(runtimeLibs))
+
+        return runtimeLibsFile
 
     def generate(self):
         tc = CMakeToolchain(self)
         tc.variables["USING_CONAN"] = True
-        tc.variables["CONAN_INSTALL_FOLDER"] = self.install_folder
+        tc.variables["CONAN_RUNTIME_LIBS_FILE"] = self._generateRuntimeLibsFile()
         if self.settings.os == "Android":
             tc.variables["CMAKE_ANDROID_API"] = str(self.settings.os.api_level)
-            tc.variables["ANDROID_SYSROOT_LIB_SUBDIR"] = {
-                'armv7': 'arm-linux-androideabi',
-                'armv8': 'aarch64-linux-android',
-                'x86': 'i686-linux-android',
-                'x86_64': 'x86_64-linux-android',
-            }.get(str(self.settings.arch))
-        if cross_building(self) and self.settings.os == "Windows":
-            tc.variables["CONAN_SYSTEM_LIBRARY_LOCATION"] = self.env["CONAN_SYSTEM_LIBRARY_LOCATION"]
-        tc.generate()
-
-        deps = CMakeDeps(self)
-        if tools.get_env("GENERATE_ONLY_BUILT_CONFIG", default=False):
-            deps.generate()
-            return
-
-        # allow using prebuilt deps with all configs
-        # credits to https://github.com/conan-io/conan/issues/11607#issuecomment-1188500937 for the workaround
-        configs = [
-            "Debug",
-            "MinSizeRel",
-            "Release",
-            "RelWithDebInfo",
-        ]
-        for config in configs:
-            print(f"generating CMakeDeps for {config}")
-            deps.configuration = config
-            deps.generate()
-
-    def imports(self):
-        if is_apple_os(self):
-            self.copy("*.dylib", "Frameworks", "lib")
+            tc.variables["SDL_JAVA_SRC_DIR"] = os.path.join(self.dependencies.host["sdl"].package_folder, "share", "java", "SDL2")
         elif self.settings.os == "Windows":
-            self.copy("*.dll", src="bin/archdatadir/plugins/platforms", dst="platforms")
-            self.copy("*.dll", src="bin/archdatadir/plugins/styles", dst="styles")
-            self.copy("*.dll", src="@bindirs", dst="", excludes="archdatadir/*")
-        elif self.settings.os == "Android":
-            self.copy("*.so", ".", "lib")
+            tc.variables["CONAN_RUNENV_SCRIPT"] = self._pathForCmake(os.path.join(self.build_folder, "conanrun.bat"))
+        tc.generate()

+ 188 - 10
config/bonuses.json

@@ -1,4 +1,14 @@
 {
+	"ARTIFACT_GROWING":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
+	"ARTIFACT_CHARGE":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"ADDITIONAL_ATTACK":
 	{
 	},
@@ -10,6 +20,31 @@
 	"ATTACKS_ALL_ADJACENT":
 	{
 	},
+	
+	"BASE_TILE_MOVEMENT_COST":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"BATTLE_NO_FLEEING":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"BEFORE_BATTLE_REPOSITION":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"BEFORE_BATTLE_REPOSITION_BLOCK":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"BIND_EFFECT":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"BLOCKS_RANGED_RETALIATION":
 	{
@@ -39,11 +74,32 @@
 	"CHARGE_IMMUNITY":
 	{
 	},
+	
+	"COMBAT_MANA_BONUS":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"CREATURE_GROWTH":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"CREATURE_GROWTH_PERCENT":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"DARKNESS":
 	{
 		"hidden": true
 	},
+	
+	"DISGUISED":
+	{
+		"hidden": true,
+		"blockDescriptionPropagation": true
+	},
 
 	"DEATH_STARE":
 	{
@@ -65,11 +121,6 @@
 	{
 	},
 
-	"DISGUISED":
-	{
-		"hidden": true
-	},
-
 	"ENCHANTER":
 	{
 	},
@@ -112,6 +163,17 @@
 			"bonusSubtype.movementTeleporting" : null,
 		}
 	},
+	
+	"FLYING_MOVEMENT":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"FREE_SHIP_BOARDING":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 
 	"FREE_SHOOTING":
 	{
@@ -119,10 +181,12 @@
 	
 	"FULL_MAP_DARKNESS":
 	{
+		"blockDescriptionPropagation": true
 	},
-	
+
 	"FULL_MAP_SCOUTING":
 	{
+		"blockDescriptionPropagation": true
 	},
 
 	"GARGOYLE":
@@ -137,6 +201,11 @@
 			"bonusSubtype.damageTypeMelee" : null,
 		}
 	},
+	
+	"GENERATE_RESOURCE":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"HATE":
 	{
@@ -150,6 +219,21 @@
 	{
 	},
 	
+	"HERO_EXPERIENCE_GAIN_PERCENT":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"HERO_SPELL_CASTS_PER_COMBAT_TURN":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
+	"IMPROVED_NECROMANCY":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"JOUSTING":
 	{
 	},
@@ -164,12 +248,19 @@
 
 	"LEARN_BATTLE_SPELL_CHANCE":
 	{
-		"hidden": true
+		"hidden": true,
+		"blockDescriptionPropagation": true
 	},
 
 	"LEARN_BATTLE_SPELL_LEVEL_LIMIT":
 	{
-		"hidden": true
+		"hidden": true,
+		"blockDescriptionPropagation": true
+	},
+	
+	"LEARN_MEETING_SPELL_LIMIT":
+	{
+		"blockDescriptionPropagation": true
 	},
 
 	"LEVEL_SPELL_IMMUNITY":
@@ -188,6 +279,11 @@
 	{
 		"creatureNature" : true
 	},
+	
+	"LUCK":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"MANA_CHANNELING":
 	{
@@ -205,6 +301,26 @@
 	{
 	},
 	
+	"MAGIC_SCHOOL_SKILL":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"MANA_PERCENTAGE_REGENERATION":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"MANA_PER_KNOWLEDGE_PERCENTAGE":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"MAX_LEARNABLE_SPELL_LEVEL":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"MECHANICAL":
 	{
 		"creatureNature" : true
@@ -214,6 +330,16 @@
 	{
 	},
 	
+	"MORALE":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
+	"MOVEMENT":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"NEGATIVE_EFFECTS_IMMUNITY" :
 	{
 	},
@@ -241,7 +367,8 @@
 
 	"NO_TERRAIN_PENALTY":
 	{
-		"hidden": true
+		"hidden": true,
+		"blockDescriptionPropagation": true
 	},
 
 	"NON_LIVING":
@@ -270,9 +397,24 @@
 	{
 	},
 	
+	"PRIMARY_SKILL":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"REBIRTH":
 	{
 	},
+	
+	"RESOURCES_CONSTANT_BOOST":
+	{
+		"blockDescriptionPropagation": true
+	},
+
+	"RESOURCES_TOWN_MULTIPLYING_BOOST":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"RETURN_AFTER_STRIKE":
 	{
@@ -282,6 +424,11 @@
 	{
 	},
 	
+	"ROUGH_TERRAIN_DISCOUNT":
+	{
+		"blockDescriptionPropagation": true
+	},
+
 	"SIEGE_WEAPON":
 	{
 		"creatureNature" : true
@@ -299,6 +446,11 @@
 	{
 	},
 	
+	"SIGHT_RADIUS":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"SOUL_STEAL":
 	{
 	},
@@ -350,6 +502,11 @@
 	"SUMMON_GUARDIANS":
 	{
 	},
+	
+	"SURRENDER_DISCOUNT":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"TWO_HEX_ATTACK_BREATH":
 	{
@@ -370,25 +527,46 @@
 	"TRANSMUTATION_IMMUNITY":
 	{
 	},
+	
+	"THIEVES_GUILD_ACCESS":
+	{
+		"blockDescriptionPropagation": true
+	},
 
 	"UNDEAD":
 	{
 		"creatureNature" : true,
 	},
 	
+	"UNDEAD_RAISE_PERCENTAGE":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"UNLIMITED_RETALIATIONS":
 	{
 	},
 
 	"VISIONS":
 	{
-		"hidden": true
+		"hidden": true,
+		"blockDescriptionPropagation": true
 	},
 	
 	"VULNERABLE_FROM_BACK":
 	{
 	},
 	
+	"WANDERING_CREATURES_JOIN_BONUS":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
+	"WATER_WALKING":
+	{
+		"blockDescriptionPropagation": true
+	},
+	
 	"WIDE_BREATH":
 	{
 	},

+ 10 - 5
config/creatures/neutral.json

@@ -512,7 +512,8 @@
 			{
 				"type" : "LUCK",
 				"val" : 1,
-				"valueType" : "INDEPENDENT_MAX"
+				"valueType" : "INDEPENDENT_MAX",
+				"description" : "PLACEHOLDER"
 			}
 		 },
 		"graphics" :
@@ -621,7 +622,8 @@
 			{
 				"type" : "NO_TERRAIN_PENALTY",
 				"subtype" : "terrain.sand",
-				"propagator" : "HERO"
+				"propagator" : "HERO",
+				"description" : "PLACEHOLDER"
 			}
 		},
 		"graphics" :
@@ -652,7 +654,8 @@
 				"subtype" : "visionsMonsters",
 				"val" : 3,
 				"valueType" : "INDEPENDENT_MAX",
-				"propagator" : "HERO"
+				"propagator" : "HERO",
+				"description" : "PLACEHOLDER"
 			},
 			"visionsHeroes" :
 			{
@@ -660,7 +663,8 @@
 				"subtype" : "visionsHeroes",
 				"val" : 3,
 				"valueType" : "INDEPENDENT_MAX",
-				"propagator" : "HERO"
+				"propagator" : "HERO",
+				"description" : "PLACEHOLDER"
 			},
 			"visionsTowns" :
 			{
@@ -668,7 +672,8 @@
 				"subtype" : "visionsTowns",
 				"val" : 3,
 				"valueType" : "INDEPENDENT_MAX",
-				"propagator" : "HERO"
+				"propagator" : "HERO",
+				"description" : "PLACEHOLDER"
 			}
 		},
 		"graphics" :

+ 5 - 0
config/schemas/bonus.json

@@ -15,6 +15,11 @@
 			"type" : "boolean",
 			"description" : "If set to true, this bonus will be considered 'creature nature' bonus, and such creature won't be automatically granted LIVING bonus"
 		},
+		
+		"blockDescriptionPropagation" : {
+			"type" : "boolean",
+			"description" : "If set to true, this ability description will not be displayed if a creature receives it by propagation"
+		},
 
 		"description" : {
 			"type" : "string"

+ 35 - 4
config/widgets/adventureMap.json

@@ -144,6 +144,22 @@
 						}
 					]
 				},
+				{
+					"type": "adventureMapContainer",
+					"hideWhen" : "mapLayerOther",
+					"area": { "top" : 0, "left": 32, "width" : 32, "height" : 32 },
+					"items" : [
+						{
+							"type": "adventureMapButton",
+							"name": "worldViewOther",
+							"image" : "adventureLayersButton",
+							"help" : "core.help.294",
+							"hotkey": "adventureToggleMapLevel",
+							"playerColored" : true,
+							"area": { "top" : 0, "left": 0, "width" : 32, "height" : 32 }
+						}
+					]
+				},
 				{
 					"type": "adventureMapButton",
 					"name": "buttonQuestLog",
@@ -513,8 +529,8 @@
 					"items" : [
 						{
 							"type": "adventureMapButton",
-							"name": "worldViewSurface",
-							"image" : "IAM003.DEF",
+							"name": "worldViewUnderground",
+							"image" : "IAM010.DEF",
 							"hotkey": "adventureToggleMapLevel",
 							"playerColored" : true,
 							"area": { "top" : 0, "left": 0, "width" : 32, "height" : 32 }
@@ -536,8 +552,23 @@
 					"items" : [
 						{
 							"type": "adventureMapButton",
-							"name": "worldViewUnderground",
-							"image" : "IAM010.DEF",
+							"name": "worldViewSurface",
+							"image" : "IAM003.DEF",
+							"playerColored" : true,
+							"hotkey": "adventureToggleMapLevel",
+							"area": { "top" : 0, "left": 0, "width" : 32, "height" : 32 }
+						}
+					]
+				},
+				{
+					"type": "adventureMapContainer",
+					"hideWhen" : "mapLayerOther",
+					"area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 },
+					"items" : [
+						{
+							"type": "adventureMapButton",
+							"name": "worldViewOther",
+							"image" : "adventureLayersButton",
 							"playerColored" : true,
 							"hotkey": "adventureToggleMapLevel",
 							"area": { "top" : 0, "left": 0, "width" : 32, "height" : 32 }

+ 1 - 0
dependencies

@@ -0,0 +1 @@
+Subproject commit b6f03bd541f19ad441ffb930a2cbd000222a50bf

+ 17 - 18
docker/BuildAndroid-aarch64.dockerfile

@@ -6,35 +6,34 @@ RUN apt-get update && apt-get install -y openjdk-17-jdk python3 pipx cmake ccach
 ENV PIPX_HOME="/opt/pipx"
 ENV PIPX_BIN_DIR="/usr/local/bin"
 ENV PIPX_MAN_DIR="/usr/local/share/man"
-RUN pipx install 'conan<2.0'
-RUN pipx install 'sdkmanager==0.6.10'
+RUN pipx install 'conan'
+RUN pipx install 'sdkmanager'
 
-RUN conan profile new conan --detect
+RUN conan profile detect
 
-RUN wget https://github.com/vcmi/vcmi-dependencies/releases/download/1.3/dependencies-android-arm64-v8a.txz
-RUN tar -xf dependencies-android-arm64-v8a.txz -C ~/.conan
-RUN rm dependencies-android-arm64-v8a.txz
+ENV DEPS_VERSION="2025-08-24"
+ENV DEPS="dependencies-android-arm64-v8a.tgz"
+RUN wget --https-only --max-redirect=20 https://github.com/vcmi/vcmi-dependencies/releases/download/$DEPS_VERSION/$DEPS
+RUN conan cache restore $DEPS
+RUN rm $DEPS
 
 ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
 ENV ANDROID_HOME="/usr/lib/android-sdk"
-ENV GRADLE_USER_HOME="/vcmi/.cache/grandle"
-ENV GENERATE_ONLY_BUILT_CONFIG=1
+ENV GRADLE_USER_HOME="/vcmi/.cache/gradle"
+ENV NDK_VERSION="25.2.9519653"
 
-RUN sdkmanager --install "platform-tools"
-RUN sdkmanager --install "platforms;android-34"
+RUN sdkmanager --install "platform-tools" "platforms;android-35" "ndk;$NDK_VERSION"
 RUN yes | sdkmanager --licenses
 
-RUN conan download android-ndk/r25c@:4db1be536558d833e52e862fd84d64d75c2b3656 -r conancenter
-
 CMD ["sh", "-c", " \
     # switch to mounted dir
     cd /vcmi ; \
-    # install conan stuff
-    conan install . --install-folder=conan-generated --no-imports --build=never --profile:build=default --profile:host=CI/conan/android-64-ndk ; \
-    # link conan ndk that grandle can find it
-    mkdir -p /usr/lib/android-sdk/ndk ; \
-    ln -s -T ~/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/lib/android-sdk/ndk/25.2.9519653 ; \
+    # prepare Gradle config
+    mkdir -p $GRADLE_USER_HOME ; \
+    echo android.bundle.enableUncompressedNativeLibs=true > $GRADLE_USER_HOME/gradle.properties ; \
+    # generate CMake toolchain
+    conan install . --output-folder=conan-generated --build=never --profile=dependencies/conan_profiles/android-64 --profile=dependencies/conan_profiles/base/android-system -c tools.android:ndk_path=$ANDROID_HOME/ndk/$NDK_VERSION && \
     # build
-    cmake --preset android-daily-release ; \
+    cmake --preset android-daily-release && \
     cmake --build --preset android-daily-release \
 "]

+ 33 - 19
docs/developers/Building_Android.md

@@ -1,19 +1,19 @@
 # Building Android
 
-The following instructions apply to **v1.2 and later**. For earlier versions the best documentation is <https://github.com/vcmi/vcmi-android/blob/master/building.txt> (and reading scripts in that repo), however very limited to no support will be provided from our side if you wish to go down that rabbit hole.
+The following instructions apply to **v1.7 and later**. For earlier versions see older Git revision, e.g. from [1.6.8 release](https://github.com/vcmi/vcmi/blob/1.6.8/docs/developers/Building_Android.md).
 
 *Note*: building has been tested only on Linux and macOS. It may or may not work on Windows out of the box.
 
 ## Requirements
 
-1. CMake 3.20+: download from your package manager or from <https://cmake.org/download/>
-2. JDK 11, not necessarily from Oracle
+1. CMake 3.26+: download from your package manager or from <https://cmake.org/download/>
+2. JDK 17, not necessarily from Oracle
 3. Android command line tools or Android Studio for your OS: <https://developer.android.com/studio/>
-4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it:
+4. Android NDK version **r25c (25.2.9519653)** (later version would also probably work), there're multiple ways to obtain it:
+    - recommended: download with Conan, especially if you're going to build VCMI dependencies from source, see [#NDK and Conan](#ndk-and-conan)
     - install with Android Studio
     - install with `sdkmanager` command line tool
     - download from <https://developer.android.com/ndk/downloads>
-    - download with Conan, see [#NDK and Conan](#ndk-and-conan)
 5. Optional:
     - Ninja: download from your package manager or from <https://github.com/ninja-build/ninja/releases>
     - Ccache: download from your package manager or from <https://github.com/ccache/ccache/releases>
@@ -32,15 +32,18 @@ We use Conan package manager to build/consume dependencies, find detailed usage
 
 On the step where you need to replace **PROFILE**, choose:
 
-- `android-32` to build for 32-bit architecture (armeabi-v7a)
-- `android-64` to build for 64-bit architecture (aarch64-v8a)
+- `android-32-ndk` to build for ARM 32-bit (armeabi-v7a)
+- `android-64-ndk` to build for ARM 64-bit (aarch64-v8a)
+- `android-x64-ndk` to build for Intel 64-bit (x86_64)
+
+Advanced users may choose profile without `-ndk` suffix to use NDK that's already installed in their system.
 
 ### NDK and Conan
 
-Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html):
+Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/2/examples/cross_build/android/ndk.html#examples-cross-build-android-ndk):
 
-- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. On the step where you need to replace **PROFILE**, choose *android-**X**-ndk* where ***X*** is either `32` or `64`.
-- to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will work only when consuming the pre-built binaries)
+- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically
+- to use an already installed NDK, you can simply pass it on the command line to `conan install`: (note that this will likely work only when consuming the prebuilt binaries)
 
 ```sh
 conan install -c tools.android:ndk_path=/path/to/ndk ...
@@ -48,37 +51,48 @@ conan install -c tools.android:ndk_path=/path/to/ndk ...
 
 ## Build process
 
+Before starting the build, local Gradle configuration must be created (it's a requirement for Qt 5):
+
+```sh
+mkdir ~/.gradle
+echo "android.bundle.enableUncompressedNativeLibs=true" > ~/.gradle/gradle.properties
+```
+
 Building for Android is a 2-step process. First, native C++ code is compiled to a shared library (unlike an executable on other platforms), then Java code is compiled to an actual executable which will be loading the native shared library at runtime.
 
-This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset.
+This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good.
 
 The Java code (located in the `android` directory of the repo) will be built automatically after the native code using the `androiddeployqt` tool. But you must set `JAVA_HOME` and `ANDROID_HOME` environment variables.
 
-APK will appear in `<build dir>/android-build/vcmi-app/build/outputs/apk/debug` directory which you can then install to your device with `adb install -r /path/to/apk` (`adb` command is from Android command line tools).
+APK will appear in `<build dir>/android-build/vcmi-app/build/outputs/apk/<build configuration>` directory which you can then install to your device with `adb install -r /path/to/apk` (`adb` command is from Android command line tools).
 
 ### Example
 
 ```sh
 # the following environment variables must be set
-export JAVA_HOME=/path/to/jdk11
+export JAVA_HOME=/path/to/jdk17
 export ANDROID_HOME=/path/to/android/sdk
 
 cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D ENABLE_CCACHE:BOOL=ON --toolchain ...
 cmake --build ../build
 ```
 
-You can also see a more detailed walkthrough on CMake configuration at [How to build VCMI (macOS)](./Building_macOS.md).
-
 ## Docker
 
 For developing it's also possible to use Docker to build android APK. The only requirement is to have Docker installed. The container image contains all the other prerequisites.
 
-To build using docker just open a terminal with `vcmi` as working directory.
+To build using docker just open a terminal with `vcmi` repo root as working directory.
 
 Build the image with (only needed once):
-`docker build -f docker/BuildAndroid-aarch64.dockerfile -t vcmi-android-build .`
+
+```sh
+docker build -f docker/BuildAndroid-aarch64.dockerfile -t vcmi-android-build .
+```
 
 After building the image you can compile vcmi with:
-`docker run -it --rm -v $PWD/:/vcmi vcmi-android-build`
 
-The current dockerfile is aarch64 only but can adjusted manually for armv7.
+```sh
+docker run -it --rm -v $PWD/:/vcmi vcmi-android-build
+```
+
+The current dockerfile is aarch64 only but can be easily adjusted for other architectures.

+ 110 - 82
docs/developers/Building_Windows.md

@@ -2,18 +2,17 @@
 
 ## Preparations
 
-Windows builds can be made in more than one way and with more than one tool. This guide focuses on the simplest building process using Microsoft Visual Studio 2022
+Windows builds can be made in more than one way and with more than one tool. This guide will show how to do it using Microsoft Visual Studio 2022 (MSVC compiler) and MSYS2 (MinGW compiler).
 
 ## Prerequisites
 
-- Windows Vista or newer.
-- [Microsoft Visual Studio](https://visualstudio.microsoft.com/downloads/)
-- Git or git GUI, for example, SourceTree [download link](http://www.sourcetreeapp.com/download)
-- CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users".
-- To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html)
-- Optional:
-  - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page)
-  - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases)
+1. Windows 7 or newer
+2. [Microsoft Visual Studio](https://visualstudio.microsoft.com/downloads/)
+3. Git or git GUI, for example, SourceTree [download link](http://www.sourcetreeapp.com/download)
+4. CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users". You can also install CMake from the Visual Studio Installer or from a package manager like [WinGet](https://github.com/microsoft/winget-cli).
+5. Optional:
+    - To create installer: [Inno Setup](https://jrsoftware.org/isinfo.php)
+    - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases)
 
 ### Choose an installation directory
 
@@ -34,92 +33,72 @@ Bad locations:
 
 ## Install VCMI dependencies
 
-You have two options: to use pre-built libraries or build your own. We strongly recommend start with using pre-built ones.
+This step is needed only for MSVC compiler. You can also find legacy Vcpkg instructions in the bottom of the document.
 
-### Option A. Use pre-built Vcpkg
+We use Conan package manager to build/consume dependencies, find detailed usage instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Conan.md).
 
-#### Download and unpack archive
+On the step where you need to replace **PROFILE**, choose:
 
-Vcpkg Archives are available at our GitHub: <https://github.com/vcmi/vcmi-deps-windows/releases>
+- `msvc-x64` to build for Intel 64-bit (x64 / x86_64)
+- `msvc-arm64` to build for ARM 64-bit (arm64)
+- `msvc-x86` to build for Intel 32-bit (x86)
 
-- Download latest version available.
-EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z)  
-- Extract archive by right clicking on it and choosing "7-zip -> Extract Here".
+*Note*: we recommend using CMD (`cmd.exe`) for the next steps. If you absolutely want to use Powershell, then run `conan install` twice appending `-c tools.env.virtualenv:powershell=powershell.exe` on the second run.
 
-#### Move dependencies to target directory
-
-Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside.
-Move extracted `vcpkg` directory into your `%VCMI_DIR%`
+## Install CCache
 
-### Option B. Build Vcpkg on your own
+Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in.
 
-Please be aware that if you're running 32-bit Windows version, then this is impossible due to <https://github.com/microsoft/vcpkg/issues/26036>
-Be aware that building Vcpkg might take a lot of time depend on your CPU model and 10-20GB of disk space.
+## Build VCMI
 
-#### Create initial directory
+### Clone VCMI repository
 
-#### Clone vcpkg
+#### From Git GUI
 
-1. open SourceTree
-2. File -\> Clone
-3. select **<https://github.com/microsoft/vcpkg/>** as source
-4. select **%VCMI_DIR%/vcpkg** as destination
-5. click **Clone**
+1. Open SourceTree
+2. File -> Clone
+3. Select `https://github.com/vcmi/vcmi/` as source
+4. Select `%VCMI_DIR%/source` as destination
+5. Expand Advanced Options and change Checkout Branch to `develop`
+6. Tick `Recursive submodules`
+7. Click Clone
 
-From command line use:
+#### From command line
 
 ```sh
-git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
+git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source
 ```
 
-#### Build vcpkg and dependencies
-
-- Run
-`%VCMI_DIR%/vcpkg/bootstrap-vcpkg.bat`
-- For 32-bit build run:
-`%VCMI_DIR%/vcpkg/vcpkg.exe install tbb:x64-windows fuzzylite:x64-windows sdl2:x64-windows sdl2-image:x64-windows sdl2-ttf:x64-windows sdl2-mixer[mpg123]:x64-windows boost:x64-windows qt5-base:x64-windows ffmpeg:x64-windows luajit:x64-windows`
-- For 64-bit build run:
-`%VCMI_DIR%/vcpkg/vcpkg.exe install install tbb:x86-windows fuzzylite:x86-windows sdl2:x86-windows sdl2-image:x86-windows sdl2-ttf:x86-windows sdl2-mixer[mpg123]:x86-windows boost:x86-windows qt5-base:x86-windows ffmpeg:x86-windows luajit:x86-windows`
-
-For the list of the packages used you can also consult [vcmi-deps-windows readme](https://github.com/vcmi/vcmi-deps-windows) in case this article gets outdated a bit.
-
-## Install CCache
-
-Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in.
-
-## Build VCMI
-
-#### From GIT GUI
+### Compile VCMI with Visual Studio
 
-- Open SourceTree
-- File -> Clone
-- select `https://github.com/vcmi/vcmi/` as source
-- select `%VCMI_DIR%/source` as destination
-- expand Advanced Options and change Checkout Branch to `develop`
-- tick `Recursive submodules`
-- click Clone
+#### Generate solution
 
-#### From command line  
+1. Open command line prompt (`cmd.exe`)
+2. Execute `cd %VCMI_DIR%`
+3. Now you need to run a script* from the Conan directory that you passed to `conan install` (the one that you passed in `--output-folder` parameter). For example, if you passed `conan-msvc`, then the script will be in `source\conan-msvc`.
+    - for CMD: `source\conan-msvc\conanrun.bat`
+    - for Powershell: `source\conan-msvc\conanrun.ps1`. If it gives an error, also run `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted`
+4. Create VS solution: `cmake -S source -B build --toolchain source\conan-msvc\conan_toolchain.cmake`
 
-- `git clone --recursive https://github.com/vcmi/vcmi.git %VCMI_DIR%/source`  
+\* This script sets up `PATH` required for Qt tools (`moc`, `uic` etc.) that run during CMake configure and build steps. Those tools depend on `zlib.dll` that was built with Conan, therefore its directory must be in `PATH`.
 
-### Generate solution for VCMI  
+#### Build solution
 
-- Create `%VCMI_DIR%/build` folder  
-- Open a command line prompt at `%VCMI_DIR%/build`  
-- Execute `cd %VCMI_DIR%/build`
-- Create solution (Visual Studio 2022 64-bit) `cmake %VCMI_DIR%/source -DCMAKE_TOOLCHAIN_FILE=%VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake -G "Visual Studio 17 2022" -A x64`
+You must launch Visual Studio in a modified `PATH` environment, see `*` in the previous subsection. You can launch it right from the current shell by pasting path to `devenv.exe` (Visual Studio executable, e.g. `"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe"`). To launch it later with correct environment, you can create batch script (a file with `bat` extension) which you can double-click, here's an example (use your own path on the first line):
 
-### Compile VCMI with Visual Studio
+```batchfile
+call "c:\Users\kamba\source\repos\vcmi\conan-msvc\conanrun.bat"
+call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe"
+```
 
-- Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio
-- Select `Release` build type in the combobox
-- If you want to use ccache:
-  - Select `Manage Configurations...` in the combobox
-  - Specify the following CMake variable: `ENABLE_CCACHE=ON`
-  - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details
-- Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer.
-- VCMI will be built in `%VCMI_DIR%/build/bin` folder!
+1. Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio
+2. Select `RelWithDebInfo` build type in the combobox
+3. If you want to use ccache:
+    - Select `Manage Configurations...` in the combobox
+    - Specify the following CMake variable: `ENABLE_CCACHE=ON`
+    - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details
+4. Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer. You can also build individual targets if you want.
+5. VCMI will be built in `%VCMI_DIR%/build/bin` folder
 
 ### Compile VCMI with MinGW via MSYS2
 
@@ -130,14 +109,6 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
 
 **NOTE:** This will link Qt5 statically to `VCMI_launcher.exe` and `VCMI_Mapeditor.exe`. See [PR #3421](https://github.com/vcmi/vcmi/pull/3421) for some background.
 
-## Create VCMI installer (This step is not required for just building & development)
-
-Make sure NSIS is installed to default directory or have registry entry so CMake can find it.
-After you build VCMI execute following commands from `%VCMI_DIR%/build`.
-
-- for release build: `cpack`
-- for debug build: `cpack -C Debug`
-
 ## Troubleshooting and workarounds
 
 Vcpkg might be very unstable due to limited popularity and fact of using bleeding edge packages (such as most recent Boost). Using latest version of dependencies could also expose both problems in VCMI code or library interface changes that developers not checked yet. So if you're built Vcpkg yourself and can't get it working please try to use binary package.
@@ -157,8 +128,65 @@ Make sure you have:
 
 ### Debug build is very slow
 
-Debug builds with MSVC are generally extremely slow since it's not just VCMI binaries are built as debug, but every single dependency too and this usually means no optimizations at all. Debug information that available for release builds is often sufficient so just avoid full debug builds unless absolutely necessary. Instead use RelWithDebInfo configuration, optionally with Optimization Disabled (/Od) to avoid variables being optimized away Also Debug configuration might have some compilation issues because it is not checked via CI for now.
+Debug builds with MSVC are generally extremely slow since it's not just VCMI binaries are built as debug, but every single dependency too and this usually means no optimizations at all. Debug information that available for release builds is often sufficient so just avoid full debug builds unless absolutely necessary. Instead use RelWithDebInfo configuration, optionally with Optimization Disabled (/Od) to avoid variables being optimized away. Also Debug configuration might have some compilation issues because it is not checked via CI for now.
 
 ### I got crash within library XYZ.dll
 
 VCPKG generated projects quite often have both debug and regular libs available to linker so it can select wrong lib. For stable RelWithDebInfo build you may try to remove debug folder from VCPKG/installed/x64-windows. Same is done on CI. Also it reduces package size at least twice.
+
+## Legacy instructions for Vcpkg package manager
+
+We have switched to the Conan package manager in v1.7. Legacy Vcpkg integration is no longer supported and may or may not work out of the box.
+
+You have two options: to use pre-built libraries or build your own. We strongly recommend start with using pre-built ones.
+
+### Option A. Use pre-built Vcpkg
+
+#### Download and unpack archive
+
+Vcpkg Archives are available at our GitHub: <https://github.com/vcmi/vcmi-deps-windows/releases>
+
+- Download latest version available.
+EG: v1.6 assets - [vcpkg-export-x64-windows-v143.7z](https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-x64-windows-v143.7z)
+- Extract archive by right clicking on it and choosing "7-zip -> Extract Here".
+
+#### Move dependencies to target directory
+
+Once extracted, a `vcpkg` directory will appear with `installed` and `scripts` subfolders inside.
+Move extracted `vcpkg` directory into your `%VCMI_DIR%`
+
+### Option B. Build Vcpkg on your own
+
+Please be aware that if you're running 32-bit Windows version, then this is impossible due to <https://github.com/microsoft/vcpkg/issues/26036>
+Be aware that building Vcpkg might take a lot of time depend on your CPU model and 10-20GB of disk space.
+
+#### Create initial directory
+
+#### Clone vcpkg
+
+1. open SourceTree
+2. File -\> Clone
+3. select **<https://github.com/microsoft/vcpkg/>** as source
+4. select **%VCMI_DIR%/vcpkg** as destination
+5. click **Clone**
+
+From command line use:
+
+```sh
+git clone https://github.com/microsoft/vcpkg.git %VCMI_DIR%/vcpkg
+```
+
+#### Build vcpkg and dependencies
+
+- Run
+`%VCMI_DIR%/vcpkg/bootstrap-vcpkg.bat`
+- For 32-bit build run:
+`%VCMI_DIR%/vcpkg/vcpkg.exe install tbb:x64-windows fuzzylite:x64-windows sdl2:x64-windows sdl2-image:x64-windows sdl2-ttf:x64-windows sdl2-mixer[mpg123]:x64-windows boost:x64-windows qt5-base:x64-windows ffmpeg:x64-windows luajit:x64-windows`
+- For 64-bit build run:
+`%VCMI_DIR%/vcpkg/vcpkg.exe install install tbb:x86-windows fuzzylite:x86-windows sdl2:x86-windows sdl2-image:x86-windows sdl2-ttf:x86-windows sdl2-mixer[mpg123]:x86-windows boost:x86-windows qt5-base:x86-windows ffmpeg:x86-windows luajit:x86-windows`
+
+For the list of the packages used you can also consult [vcmi-deps-windows readme](https://github.com/vcmi/vcmi-deps-windows) in case this article gets outdated a bit.
+
+### CMake integration
+
+When executing `cmake` configure step, pass `--toolchain %VCMI_DIR%/vcpkg/scripts/buildsystems/vcpkg.cmake`

+ 4 - 6
docs/developers/Building_iOS.md

@@ -18,10 +18,9 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git
 
 ## Obtaining dependencies
 
-There are 2 ways to get prebuilt dependencies:
+The primary and officially supported way is [Conan package manager](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Conan.md).
 
-- [Conan package manager](./Conan.md) - recommended. Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Conan.md).
-- [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) - can be used if you have Xcode 11/12 or to build for simulator / armv7 device
+There are also [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) which can be used if you have Xcode 11/12 or to build for simulator / armv7 device, but this way is no longer supported. Using Conan will also let you build with any Xcode version and for any architecture / SDK.
 
 ## Configuring project
 
@@ -30,9 +29,8 @@ Only Xcode generator (`-G Xcode`) is supported!
 As a minimum, you must pass the following variables to CMake:
 
 - `BUNDLE_IDENTIFIER_PREFIX`: unique bundle identifier prefix, something like `com.MY-NAME`
-- (if using legacy dependencies) `CMAKE_PREFIX_PATH`: path to the downloaded dependencies, e.g. `~/Downloads/vcmi-ios-depends/build/iphoneos`
 
-There're a few [CMake presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html): for device (Conan and legacy dependencies) and for simulator, named `ios-device-conan`, `ios-device` and `ios-simulator` respectively. You can also create your local "user preset" to avoid typing variables each time, see example [here](https://gist.github.com/kambala-decapitator/59438030c34b53aed7d3895aaa48b718).
+There's a [CMake preset](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html) for device named `ios-device-conan`. You can also create your local "user preset" to avoid typing variables each time, see example [here](https://gist.github.com/kambala-decapitator/59438030c34b53aed7d3895aaa48b718).
 
 Open terminal and `cd` to the directory with source code. Configuration example for device with Conan:
 
@@ -76,4 +74,4 @@ Invoke `cpack` after building:
 
 `cpack -C Release`
 
-This will generate file with extension **ipa** if you use CMake 3.25+and **zip** otherwise (simply change extension to ipa).
+This will generate file with extension **ipa** if you use CMake 3.25+ and **zip** otherwise (simply change extension to ipa).

+ 23 - 48
docs/developers/Building_macOS.md

@@ -23,7 +23,9 @@ git clone --recurse-submodules https://github.com/vcmi/vcmi.git
 
 There're 2 ways to get dependencies automatically.
 
-### Conan package manager
+### Conan package manager (recommended)
+
+We use this to produce builds on CI.
 
 Please find detailed instructions [here](./Conan.md). Note that the link points to the state of the current branch, for the latest release check the same document in the [master branch](https://github.com/vcmi/vcmi/blob/master/docs/developers/Conan.md).
 
@@ -32,47 +34,42 @@ On the step where you need to replace **PROFILE**, choose:
 - if you're on an Intel Mac: `macos-intel`
 - if you're on an Apple Silicon Mac: `macos-arm`
 
-Note: if you wish to build 1.0 release in non-`Release` configuration, you should define `USE_CONAN_WITH_ALL_CONFIGS=1` environment variable when executing `conan install`.
-
 ### Homebrew
 
 1. [Install Homebrew](https://brew.sh/)
 2. Install dependencies: `brew install boost minizip sdl2 sdl2_image sdl2_mixer sdl2_ttf tbb`
-3. If you want to watch in-game videos, also install FFmpeg: `brew install ffmpeg@4`
-4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher):
+3. If you want to watch in-game videos, also install FFmpeg: `brew install ffmpeg` (you can also use an earlier FFmpeg version)
+4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher and Map editor):
     - `brew install qt@5` for Qt 5 or `brew install qt` for Qt 6
-    - using [Qt Online Installer](https://www.qt.io/download) - choose **Go open source**
+    - using [Qt Online Installer](https://www.qt.io/download-qt-installer-oss)
 
 ## Preparing build environment
 
 This applies only to Xcode-based toolchain. If `xcrun -f clang` prints errors, then use either of the following ways:
 
 - select an Xcode instance from Xcode application - Preferences - Locations - Command Line Tools
-- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path: for example, `sudo xcode-select -s /Library/Developer/CommandLineTools`
-- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path: for example, `export DEVELOPER_DIR=/Applications/Xcode.app`
+- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path, example: `sudo xcode-select -s /Library/Developer/CommandLineTools`
+- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path, example: `export DEVELOPER_DIR=/Applications/Xcode.app`
 
 ## Configuring project for building
 
-Note that if you wish to use Qt Creator IDE, you should skip this step and configure respective variables inside the IDE.
+Note that if you wish to use Qt Creator or CLion IDE, you should skip this step and configure respective variables inside the IDE. Or you could create a [CMake preset](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html) to avoid manual configuration.
+
+The following walkthrough lists only the bare minimum of required CMake options.
 
 1. In Terminal `cd` to the source code directory
 2. Start assembling CMake invocation: type `cmake -S . -B BUILD_DIR` where *BUILD_DIR* can be any path, **don't press Return**
 3. Decide which CMake generator you want to use:
-    - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'`
-    - Ninja (if you have installed it): pass `-G Ninja`
     - Xcode IDE (if you have installed it): pass `-G Xcode`
-4. If you picked Makefiles or Ninja, pick desired *build type* - either of Debug / RelWithDebInfo / Release / MinSizeRel - and pass it in `CMAKE_BUILD_TYPE` option, for example: `-D CMAKE_BUILD_TYPE=Release`. If you don't pass this option,     `RelWithDebInfo` will be used.
-5. If you don't want to build Launcher, pass `-D ENABLE_LAUNCHER=OFF`
-6. You can also pass `-Wno-dev` if you're not interested in CMake developer warnings
-7. Next step depends on the dependency manager you have picked:
-    - Conan: pass `-D CMAKE_TOOLCHAIN_FILE=conan-generated/conan_toolchain.cmake` where **conan-generated** must be replaced with your directory choice
-    - Homebrew: if you installed FFmpeg or Qt 5, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon):
-        - if you installed FFmpeg, insert `$(brew --prefix ffmpeg@4)`
-        - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)`
-        - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64`
-        - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"`
-8. If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON`
-9. Now press Return
+    - Ninja (if you have installed it): pass `-G Ninja`
+    - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'`
+4. If you picked Makefiles or Ninja, pick desired *build type* - either of `Debug` / `RelWithDebInfo` / `Release` / `MinSizeRel` - and pass it in `CMAKE_BUILD_TYPE` option, example: `-D CMAKE_BUILD_TYPE=Debug`. If you use don't pass this option, `RelWithDebInfo` will be used.
+5. Next step depends on the dependency manager you have picked:
+    - Conan: pass `--toolchain conan-generated/conan_toolchain.cmake` (or via `CMAKE_TOOLCHAIN_FILE` variable) where **conan-generated** must be replaced with your directory choice
+    - Homebrew: if you installed Qt 5 or from the Online Installer, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon):
+        - if you installed Qt 5 from Homebrew, insert `$(brew --prefix qt@5)`
+        - if you installed Qt from Online Installer, insert your path to Qt directory, example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64`
+6. Now press Return
 
 ## Building project
 
@@ -87,35 +84,13 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h
 `cmake --build <path to build directory>`
 
 - If using Makefiles generator, you'd want to utilize all your CPU cores by appending `-- -j$(sysctl -n hw.ncpu)` to the above
-- If using Xcode generator, you can also choose which configuration to build by appending `--config <configuration name>` to the above, for example: `--config Debug`
-
-## Packaging project into DMG file
-
-After building, run `cpack` from the build directory. If using Xcode generator, also pass `-C <configuration name>` with the same configuration that you used to build the project.
-
-If you use Conan, it's expected that you use **conan-generated** directory at step 4 of [Conan package manager](Conan.md).
+- If using Xcode generator, you can also choose which configuration to build by appending `--config <configuration name>` to the above, example: `--config Debug`
 
 ## Running VCMI
 
-You can run VCMI from DMG, but it's will also work from your IDE be it Xcode or Qt Creator.
-
-Alternatively you can run binaries directly from the **bin** directory:
+You can run binaries from your IDE or directly from the **bin** directory:
 
 - BUILD_DIR/bin/vcmilauncher
 - BUILD_DIR/bin/vcmiclient
 - BUILD_DIR/bin/vcmiserver
-
-CMake include commands to copy all needed assets from source directory into the **bin** directory on each build. They'll work when you build from Xcode too.
-
-## Some useful debugging tips
-
-Anyone who might want to debug builds, but new to macOS could find following commands useful:
-
-- To attach DMG file from command line use `hdiutil attach vcmi-1.0.dmg`
-- Detach volume: `hdiutil detach /Volumes/vcmi-1.0`
-- To view dependency paths: `otool -L /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient`
-- To display load commands such as `LC_RPATH`: `otool -l /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient`
-
-## Troubleshooting
-
-In case of troubles you can always consult our CI build scripts or contact the dev team via discord.
+- BUILD_DIR/bin/vcmimapeditor

+ 184 - 99
docs/developers/Conan.md

@@ -6,137 +6,255 @@
 
 The following platforms are supported and known to work, others might require changes to our [conanfile.py](../../conanfile.py) or upstream recipes.
 
-- **macOS**: x86_64 (Intel) - target 10.13 (High Sierra), arm64 (Apple Silicon) - target 11.0 (Big Sur)
+- **macOS**:
+  - x86_64 (Intel) - target 10.13 (High Sierra)
+  - arm64 (Apple Silicon) - target 11.0 (Big Sur)
 - **iOS**: arm64 - target 12.0
-- **Windows**: x86_64 and x86 fully supported with building from Linux
-- **Android**: armeabi-v7a (32-bit ARM) - target 4.4 (API level 19), aarch64-v8a (64-bit ARM) - target 5.0 (API level 21)
+- **Windows** using MSVC compiler:
+  - x86_64 (x64) - target Windows 7
+  - x86 - target Windows 7
+  - arm64 - target Windows 11
+- **Android**:
+  - arvm7 / armeabi-v7a (32-bit ARM) - target 4.4 (API level 19)
+  - arm64 / aarch64-v8a (64-bit ARM) - target 5.0 (API level 21)
+  - x86_64 (64-bit Intel) - target 5.0 (API level 21)
 
 ## Getting started
 
-1. [Install Conan](https://docs.conan.io/1/installation.html). Currently we support only Conan v1, you can install it with `pip` like this: `pip3 install 'conan<2.0'`
-2. Execute in terminal: `conan profile new default --detect`
+1. [Install Conan](https://docs.conan.io/2/installation.html). For example: `pip3 install conan`
+2. Execute in terminal: `conan profile detect`. It will create *build profile* for your machine.
 
 ## Download dependencies
 
 0. If your platform is not on the list of supported ones or you don't want to use our prebuilt binaries, you can still build dependencies from source or try consuming prebuilt binaries from the central Conan repository - [ConanCenter](https://conan.io/center/). In this case skip to the [next section](#generate-cmake-integration) directly.
 
 1. Check if your build environment can use the prebuilt binaries: basically, that your compiler version (or Xcode major version) matches the information below. If you're unsure, simply advance to the next step.
+    - *macOS*: libraries are built with Apple clang 16 (Xcode 16.2), should be consumable by Xcode / Xcode CLT 14.x and later
+    - *iOS*: libraries are built with Apple clang 16 (Xcode 16.2), should be consumable by Xcode 14.x and later
+    - *Windows*: libraries are built with MSVC 19.4x (v143 toolset)
+    - *Android*: libraries are built with NDK r25c (25.2.9519653)
 
-    - **macOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode / Xcode CLT 14.x and later (older library versions are also available for Xcode 13, see Releases in the respective repo)
-    - **iOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode 14.x and later (older library versions are also available for Xcode 13, see Releases in the respective repo)
-    - **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04)
-    - **Android**: libraries are built with NDK r25c (25.2.9519653)
+2. Download the binaries archive from <https://github.com/vcmi/vcmi-dependencies/releases> (pre-release is for development version and the latest release is for respective VCMI release) and use `conan cache restore <path to archive>` command to unpack them.
+    - *macOS*: pick **dependencies-mac-intel.tgz** if you have Intel Mac, otherwise - **dependencies-mac-arm.tgz**
+    - *iOS*: pick **dependencies-ios.tgz**
+    - *Windows*: pick **dependencies-windows-x64.tgz** for Windows x64, **dependencies-windows-arm64.tgz** for Windows ARM64 or **dependencies-windows-x86.tgz** for Windows x86
+    - *Android*: pick **dependencies-android-arm64-v8a.tgz** for arm64 (ARM 64-bit), **dependencies-android-armeabi-v7a.tgz** for armv7 (ARM 32-bit) or **dependencies-android-x64.tgz** for x86_64 (Intel 64-bit)
 
-2. Download the binaries archive and unpack it to `~/.conan` directory from <https://github.com/vcmi/vcmi-dependencies/releases/latest>
+### Platform-specific preparation
 
-    - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz**
-    - iOS: pick ***dependencies-ios.txz***
-    - Windows: currently only mingw is supported. Pick **dependencies-mingw.tgz** if you want x86_64, otherwise pick **dependencies-mingw-32.tgz**
-    - Android: current limitation is that building works only on a macOS host due to Qt 5 for Android being compiled on macOS. Simply delete directory `~/.conan/data/qt/5.15.x/_/_/package` (`5.15.x` is a placeholder) after unpacking the archive and build Qt from source. Alternatively, if you have (or are [willing to build](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs)) Qt host tools for your platform, then simply replace those in the archive with yours and most probably it would work.
+Follow this subsection only if any of the following applies to you, otherwise skip to the [next section](#generate-cmake-integration) directly.
 
-3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS:
+- you're trying to build for Android and your OS is Windows or macOS or Linux aarch64
+- you're trying to build for iOS and you have Intel Mac
 
-    1. Open file `~/.conan/data/qt/5.15.x/_/_/export/conanfile.py` (`5.15.x` is a placeholder), search for string `Designer` (there should be only one match), comment this line and the one above it by inserting `#` in the beginning, and save the file.
-    2. (optional) If you don't want to use Rosetta, follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders). Make sure **not** to copy `qt.conf`!
+Qt 5 has some utilities required for the build process (moc, uic etc.) that are built for your desktop OS. Our CI (GitHub Actions) makes Android builds on Ubuntu Linux amd64 and iOS builds on an Apple Silicon Mac, therefore those Qt utilities are built for Linux amd64 and macOS arm64 respectively and can't be used/run on other OS / CPU architectures. To solve that, you must add/replace those utilities built for your desktop OS.
+
+Once you have the executable files of those utilities, copy them to the `bin` directory of Qt package. You can find the package at `~/.conan2/p/qt<some hash>/p`.
+
+#### Option 1
+
+The easiest way would be downloading prebuilt dependencies for your platform (Windows / macOS), unpacking the archive (using `conan cache restore` isn't required, unpack as an ordinary archive) and copying (or making a hard or soft link) executable files from `<unpacked dir>/b/qt<some hash>/p/bin` directory. We don't provide prebuilts for Linux - simply install Qt development package from your package manager and copy executables from its `bin` directory. But you can also build all the executables manually, see [below](#option-2).
+
+However, Qt for Android requires one more executable that can't be found in your desktop Qt build - `androiddeployqt`. And you'll have to build it manually, see next subsection.
+
+#### Option 2
+
+Building all those executables is rather fast as it doesn't require building whole Qt. This is how you do it in Bash shell:
+
+```sh
+# set Qt version that you're going to download and build
+qtVer=5.15.16
+
+# for Android:
+# ensure that ANDROID_HOME environment variable is set pointing to Android SDK directory
+# also set Min SDK and NDK versions
+minSdkVersion=21
+ndkVersion=25.2.9519653
+
+qtSrcDir="qt-everywhere-src-$qtVer"
+qtInstallDir="$(pwd)/install"
+
+# download Qt sources, unpack only the required subset of them
+# use URL from a mirror if needed
+curl -L "https://download.qt.io/official_releases/qt/5.15/$qtVer/single/qt-everywhere-opensource-src-$qtVer.tar.xz" \
+  | tar -xf - --xz "$qtSrcDir"/{'.*','LICENSE*','configure*',qt.pro,'qtbase/*','qttools/*'}
+
+# create build directory
+mkdir build
+cd build
+
+# configure Qt for building, also pass to the following command:
+# for Android: -shared -xplatform android-clang -android-sdk "$ANDROID_HOME" -android-ndk "$ANDROID_HOME/ndk/$ndkVersion" -android-ndk-platform android-$minSdkVersion -android-abis arm64-v8a
+# for iOS: -static -xplatform macx-ios-clang -no-framework
+"../$qtSrcDir/configure" -prefix "$qtInstallDir" -opensource -confirm-license -release -strip -appstore-compliant -make libs -no-compile-examples -no-dbus -system-zlib -no-openssl -opengl es2 -no-gif -no-ico -nomake examples -skip qt3d -skip qtactiveqt -skip qtandroidextras -skip qtcharts -skip qtconnectivity -skip qtdatavis3d -skip qtdeclarative -skip qtdoc -skip qtgamepad -skip qtgraphicaleffects -skip qtimageformats -skip qtlocation -skip qtlottie -skip qtmacextras -skip qtmultimedia -skip qtnetworkauth -skip qtpurchasing -skip qtquick3d -skip qtquickcontrols -skip qtquickcontrols2 -skip qtquicktimeline -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors -skip qtserialbus -skip qtserialport -skip qtspeech -skip qtsvg -skip qttranslations -skip qtvirtualkeyboard -skip qtwayland -skip qtwebchannel -skip qtwebengine -skip qtwebglplugin -skip qtwebsockets -skip qtwebview -skip qtwinextras -skip qtx11extras -skip qtxmlpatterns
+
+make --jobs=$(getconf _NPROCESSORS_ONLN) module-qtbase-qmake_all module-qttools-qmake_all
+
+# if you need to build just androiddeployqt, on the second line pass only: sub-androiddeployqt
+make --directory=qtbase/src --jobs=$(getconf _NPROCESSORS_ONLN) \
+  sub-bootstrap sub-moc sub-qlalr sub-qvkgen sub-rcc sub-tracegen sub-uic sub-androiddeployqt
+
+make --directory=qttools/src/linguist --jobs=$(getconf _NPROCESSORS_ONLN) \
+  sub-lconvert sub-lprodump sub-lrelease sub-lrelease-pro sub-lupdate sub-lupdate-pro
+```
+
+After that you'll find all the executables in `build/qtbase/bin` and `build/qttools/bin` directories.
+
+#### Option 3
+
+Simply build whole Qt from source.
+
+1. Remove Qt binary package: `conan remove "qt/5.15.*"`
+2. In the next section use `--build=missing`
 
 ## Generate CMake integration
 
-Conan needs to generate CMake toolchain file to make dependencies available to CMake. See `CMakeDeps` and `CMakeToolchain` [in the official documentation](https://docs.conan.io/1/reference/conanfile/tools/cmake.html) for details.
+Conan needs to generate CMake toolchain file to make dependencies available to CMake. See `CMakeDeps` and `CMakeToolchain` [in the official documentation](https://docs.conan.io/2/reference/tools/cmake.html) for details.
+
+Make sure that you've cloned VCMI repository with submodules! (or initialized them afterwards)
+
+In terminal `cd` to the VCMI source directory and run the following command (it's written in Bash syntax, for other shells like Cmd or Powershell use appropriate line continuation character instead of `\` or type everything on a single line). Also check subsections for additional requirements on consuming prebuilt binaries.
 
-In terminal `cd` to the VCMI source directory and run the following command. Also check subsections for additional requirements on consuming prebuilt binaries.
+*Note*: if you're going to build for Windows MSVC, it's recommended to use Cmd shell. If you absolutely want to use Powershell, then run the below command twice appending `-c tools.env.virtualenv:powershell=powershell.exe` on the second run.
 
 <pre>
 conan install . \
-  --install-folder=<b><i>conan-generated</i></b> \
-  --no-imports \
+  --output-folder=<b><i>conan-generated</i></b> \
   --build=<b><i>never</i></b> \
-  --profile:build=default \
-  --profile:host=<b><i>CI/conan/PROFILE</i></b>
+  --profile=<b><i>dependencies/conan_profiles/PROFILE</i></b> \
+  <b><i>EXTRA PARAMS</i></b>
 </pre>
 
 The highlighted parts can be adjusted:
 
 - ***conan-generated***: directory (absolute or relative) where the generated files will appear. This value is used in CMake presets from VCMI, but you can actually use any directory and override it in your local CMake presets.
-- ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build recipes, that are not present in your local cache, from source.
-- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile).
-- ***note for Windows x86***: use profile mingw32-linux.jinja for building instead of mingw64-linux.jinja
+- ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build binary packages, that are not present in your local cache, from source. There're also other values, see `conan install -h` or the full documentation linked below.
+- ***dependencies/conan_profiles/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../../dependencies/conan_profiles) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile, will build for your desktop OS) or create your own profile for the desired platform.
+- ***EXTRA PARAMS***: additional params to the `conan install` command, you can specify multiple:
 
-If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead.
-
-VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this).
+  - if you want to consume our prebuilt binaries for Apple platforms (macOS / iOS), pass `--profile=dependencies/conan_profiles/base/apple-system`
+  - if you want to consume our prebuilt binaries for Android, pass `--profile=dependencies/conan_profiles/base/android-system`
+  - if your intention is to make a Debug build, pass `-s "&:build_type=RelWithDebInfo"` for Windows MSVC and `-s "&:build_type=Debug"` for other platforms
+  - if you're building on Windows 11 ARM64, pass `-o "&:lua_lib=lua"`
+  - if you're building on (or for) Windows < 10, pass `-o "&:target_pre_windows10=True"`
 
-*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/1/reference/commands/consumer/install.html) or by executing `conan help install`.
+If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead.
 
-### Using our prebuilt binaries for macOS/iOS
+VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o "&:with_ffmpeg=False"`. Check [the recipe](../../dependencies/conanfile.py) for details.
 
-We use custom recipes for some libraries that are provided by the OS. You must additionally pass `-o with_apple_system_libs=True` for `conan install` to use them.
+*Note*: you can find full reference of this command [in the official documentation](https://docs.conan.io/2/reference/commands/install.html) or by executing `conan install -h`.
 
 ### Using prebuilt binaries from ConanCenter
 
 First, check if binaries for [your platform](https://github.com/conan-io/conan-center-index/blob/master/docs/supported_platforms_and_configurations.md) are produced.
 
-You must adjust the above `conan install` command:
-
-1. Replace ***CI/conan/PROFILE*** with `default`.
-2. Additionally pass `-o default_options_of_requirements=True`: this disables all custom options of our `conanfile.py` to match ConanCenter builds.
+You must adjust the above `conan install` command by replacing ***dependencies/conan_profiles/PROFILE*** with `default` (or simply omit the `--profile` parameter).
 
 ### Building dependencies from source
 
-This subsection describes platform specifics to build libraries from source properly.
+This subsection describes platform specifics to build libraries from source properly. Commands that our CI uses to build the dependencies can also be used as a reference, you can find them inside the [`dependencies` submodule](../../dependencies/.github/workflows/rebuildDependencies.yml) or in the [repository](https://github.com/vcmi/vcmi-dependencies/blob/main/.github/workflows/rebuildDependencies.yml).
+
+You can use our Conan profiles or create your own (e.g. with different options), the choice is yours.
+
+*Note:* our profiles expect you to have CMake and Ninja in `PATH`, otherwise build would fail.
 
 #### Building for macOS/iOS
 
-- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your *host* profile or pass `-s compiler.cppstd=11` on the command line.
-- If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards.
+If you wish to build dependencies against system libraries (like our prebuilts do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories.
+
+If you're going to build for iOS without using our profile: to build Qt 5 with `md4c` library, make sure to enforce this library's version 0.5 or later. This is what we have in our profile:
+
+> [replace_requires]\
+> md4c/0.4.8: md4c/0.5.2
 
 #### Building for Android
 
+It's highly recommended to use NDK recipe provided by Conan, otherwise build may fail. If you're using your own Conan profile, you can include our [NDK profile](../../dependencies/conan_profiles/base/android-ndk) via an additional `--profile` parameter on the command line.
+
 Android has issues loading self-built shared zlib library because binary name is identical to the system one, so we enforce using the OS-provided library. To achieve that, follow [below instructions](#using-recipes-for-system-libraries), you only need `zlib` directory.
 
-Also, Android requires a few Qt patches. They are needed only for specific use cases, so you may evaluate whether you need them. The patches can be found [here](https://github.com/kambala-decapitator/Qt5-iOS-patches/tree/master/5.15.14/android). To apply selected patch(es), let Conan finish building dependencies first. Then `cd` to `qtbase` **source directory** (e.g. `~/.conan/data/qt/5.15.14/_/_/source/qt5/qtbase`) and run `patch -p1 < /path/to/patch` for each patch file (on Windows you'll need some sort of GNU environment like Git Bash to access `patch` utility).
+Also, Android requires a few patches, but they are needed only for specific use cases, so you may evaluate whether you need them. The patches can be found inside the [`dependencies` submodule](../../dependencies/conan_patches/qt/patches) or in the [repository](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_patches/qt/patches).
 
-1. [Safety measure for Xiaomi devices](https://github.com/kambala-decapitator/Qt5-iOS-patches/blob/master/5.15.14/android/xiaomi.diff). It's unclear whether it's really needed, but without it [I faced a crash once](https://bugreports.qt.io/browse/QTBUG-111960).
-2. [Fix running on Android 5.0-5.1](https://github.com/kambala-decapitator/Qt5-iOS-patches/blob/master/5.15.14/android/android-21-22.diff) (API level 21-22).
-3. [Enable running on Android 4.4](https://github.com/kambala-decapitator/Qt5-iOS-patches/blob/master/5.15.14/android/android-19-jar.diff) (API level 19-20, 32-bit only). You must also apply [one more patch](https://github.com/kambala-decapitator/Qt5-iOS-patches/blob/master/5.15.14/android/android-19-java.diff), but you must `cd` to the **package directory** for that, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH`.
+##### Qt patches
 
-After applying patch(es):
+1. [Safety measure for Xiaomi devices](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_patches/qt/patches/xiaomi.diff). It's unclear whether it's really needed, but without it [I faced a crash once](https://bugreports.qt.io/browse/QTBUG-111960).
+2. [Fix running on Android 5.0-5.1](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_patches/qt/patches/android-21-22.diff) (API level 21-22).
+3. Enable running on Android 4.4 (API level 19-20, 32-bit only): [patch 1](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_patches/qt/patches/android-19-jar.diff), [patch 1](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_patches/qt/patches/android-19-java.diff).
 
-1. `cd` to `qtbase/src/android/jar` in the **build directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/build/SOME_HASH/build_folder/qtbase/src/android/jar`.
-2. Run `make`
-3. Copy file `qtbase/jar/QtAndroid.jar` from the build directory to the **package directory**, e.g. `~/.conan/data/qt/5.15.14/_/_/package/SOME_HASH/jar`.
+##### Patches for other libraries
 
-*Note*: if you plan to build Qt from source again, then you don't need to perform the above *After applying patch(es)* steps after building.
+- Flac requires patch to be able to build it for 32-bit targeting API Level < 24
+- Minizip requires patch to be able to build it for 32-bit targeting API Level < 21
 
-##### Using recipes for system libraries
+Also, to build Flac, Luajit and Opusfile for 32-bit targeting API Level < 24, a couple of C defines must be added to your Conan profile - see our [profile for ARM 32-bit](https://github.com/vcmi/vcmi-dependencies/blob/main/conan_profiles/android-32).
+
+#### Using recipes for system libraries
 
 1. Clone/download <https://github.com/kambala-decapitator/conan-system-libs>
-2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android. Do it for each library you need.
-3. Now you can execute `conan install` to build all dependencies.
+2. Execute `conan create PACKAGE --user system`, where `PACKAGE` is a directory path in that repository. Do it for each library you need. (basically just read repo's readme)
+
+#### Build it
+
+First, you must build separate libraries with `conan create` if:
+
+- you chose to use patches (ours listed above or your own) for a library. You must also pass `--core-conf core.sources.patch:extra_path=<patches path>` parameter where `<patches path>` is the path to the patches directory, ours is located at [dependencies/conan_patches](../../dependencies/conan_patches).
+- you want to build LuaJIT for iOS or Android (they also require patches, see the above point). Upstream Conan recipe doesn't support this yet (but there's a [pull request](https://github.com/conan-io/conan-center-index/pull/26577)), so you'll have to use [fork](https://github.com/kambala-decapitator/conan-center-index/tree/package/luajit) where it works. *Note*: to build for 32-bit architecture (e.g. Android armv7) your OS must be able to run 32-bit executables, see [this issue](https://github.com/LuaJIT/LuaJIT/issues/664) for details (for example, macOS since 10.15 can't do that); on Linux amd64 you'll have to install `libc6-dev-i386` package.
+
+After that you can execute `conan install` to build the rest of the dependencies.
 
 ## Configure project for building
 
 You must pass the generated toolchain file to CMake invocation.
 
-- if using custom CMake presets, just make sure to inherit our `build-with-conan` preset. If you store Conan generated files in a non-default directory, define the path to the generated toolchain in `toolchainFile` field (or `CMAKE_TOOLCHAIN_FILE` cache variable) or include CMake presets file generated by Conan.
+- if using custom CMake presets, define the path to the generated toolchain in `toolchainFile` field (or `CMAKE_TOOLCHAIN_FILE` cache variable) or include CMake presets file generated by Conan.
 - otherwise, if passing CMake options on the command line, use `--toolchain` option (available in CMake 3.21+) or `CMAKE_TOOLCHAIN_FILE` variable.
 
 ## Examples
 
 In these examples only the minimum required amount of options is passed to `cmake` invocation, you can pass additional ones as needed.
 
-### Use our prebuilt binaries to build for macOS x86_64 with Xcode
+### Use our prebuilt binaries to build for Windows x64 with Visual Studio (CMD shell)
+
+```batchfile
+conan install . ^
+  --output-folder=conan-msvc ^
+  --build=never ^
+  --profile=dependencies\conan_profiles\msvc-x64 ^
+  -s "&:build_type=RelWithDebInfo"
+
+REM this is important!
+conan-msvc\conanrun.bat
+
+cmake -S . -B build ^
+  --toolchain conan-msvc\conan_toolchain.cmake
+```
+
+### Use our prebuilt binaries to build for macOS arm64 with Xcode
 
 ```sh
 conan install . \
-  --install-folder=conan-generated \
-  --no-imports \
+  --output-folder=conan-macos \
   --build=never \
-  --profile:build=default \
-  --profile:host=CI/conan/macos-intel \
-  -o with_apple_system_libs=True
+  --profile=dependencies/conan_profiles/macos-arm \
+  --profile=dependencies/conan_profiles/base/apple-system \
+  -s "&:build_type=Debug"
 
 cmake -S . -B build -G Xcode \
-  --toolchain conan-generated/conan_toolchain.cmake
+  --toolchain conan-macos/conan_toolchain.cmake
+```
+
+### Use our prebuilt binaries to build for Android arm64 with Ninja
+
+```sh
+conan install . \
+  --output-folder=conan-android64 \
+  --build=never \
+  --profile=dependencies/conan_profiles/android-64-ndk \
+  --profile=dependencies/conan_profiles/base/android-system \
+  -s "&:build_type=Debug"
+
+cmake -S . -B build -G Ninja \
+  --toolchain conan-android64/conan_toolchain.cmake
 ```
 
 ### Try to use binaries from ConanCenter for your platform
@@ -146,11 +264,8 @@ If you also want to build the missing binaries from source, use `--build=missing
 ```sh
 conan install . \
   --install-folder=~/my-dir \
-  --no-imports \
   --build=never \
-  --profile:build=default \
-  --profile:host=default \
-  -o default_options_of_requirements=True
+  -s "&:build_type=Debug"
 
 cmake -S . -B build \
   -D CMAKE_TOOLCHAIN_FILE=~/my-dir/conan_toolchain.cmake
@@ -160,12 +275,11 @@ cmake -S . -B build \
 
 ```sh
 conan install . \
-  --install-folder=~/my-dir \
-  --no-imports \
+  --output-folder=conan-ios \
   --build=never \
-  --profile:build=default \
-  --profile:host=CI/conan/ios-arm64 \
-  -o with_apple_system_libs=True
+  --profile=dependencies/conan_profiles/ios-arm64 \
+  --profile=dependencies/conan_profiles/base/apple-system \
+  -s "&:build_type=Debug"
 
 cmake --preset ios-conan
 ```
@@ -184,8 +298,8 @@ cmake --preset ios-conan
         {
             "name": "ios-conan",
             "displayName": "iOS",
-            "inherits": ["build-with-conan", "ios-device"],
-            "toolchainFile": "~/my-dir/conan_toolchain.cmake",
+            "inherits": ["ios-device"],
+            "toolchainFile": "conan-ios/conan_toolchain.cmake",
             "cacheVariables": {
                 "BUNDLE_IDENTIFIER_PREFIX": "com.YOUR-NAME",
                 "CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM": "YOUR_TEAM_ID"
@@ -194,32 +308,3 @@ cmake --preset ios-conan
     ]
 }
 ```
-
-### Build VCMI with all deps for 32-bit windows in Ubuntu 22.04 WSL
-
-```powershell
-wsl --install
-wsl --install -d Ubuntu
-ubuntu
-```
-
-Next steps are identical both in WSL and in real Ubuntu 22.04
-
-```sh
-sudo pip3 install conan
-sudo apt install cmake build-essential
-sed -i 's/x86_64-w64-mingw32/i686-w64-mingw32/g' CI/mingw-ubuntu/before-install.sh
-sed -i 's/x86-64/i686/g' CI/mingw-ubuntu/before-install.sh
-sudo ./CI/mingw-ubuntu/before-install.sh
-conan install . \
-  --install-folder=conan-generated \
-  --no-imports \
-  --build=missing \
-  --profile:build=default \
-  --profile:host=CI/conan/mingw32-linux \
-  -c tools.cmake.cmaketoolchain.presets:max_schema_version=2
-cmake --preset windows-mingw-conan-linux
-cmake --build --preset windows-mingw-conan-linux --target package
-```
-
-After that, you will have functional VCMI installer for 32-bit windows.

+ 6 - 8
launcher/CMakeLists.txt

@@ -1,4 +1,6 @@
 if(ENABLE_INNOEXTRACT)
+	# libiconv not required for our use case
+	set(WITH_CONV "builtin" CACHE STRING "" FORCE)
 	add_subdirectory("lib/innoextract")
 endif()
 
@@ -25,7 +27,7 @@ set(launcher_SRCS
 		updatedialog_moc.cpp
 		prepare.cpp
 )
-if(APPLE_IOS)
+if(IOS)
 	list(APPEND launcher_SRCS
 		ios/launchGame.m
 		ios/revealdirectoryinfiles.h
@@ -153,6 +155,7 @@ if(ENABLE_SINGLE_APP_BUILD OR ANDROID)
 	add_library(vcmilauncher OBJECT ${launcher_QM})
 else()
 	add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_ICON})
+	vcmi_create_exe_shim(vcmilauncher)
 endif()
 
 if(ENABLE_TRANSLATIONS)
@@ -222,7 +225,7 @@ endif()
 
 if(ANDROID)
 	target_link_libraries(vcmilauncher Qt${QT_VERSION_MAJOR}::AndroidExtras)
-elseif(NOT APPLE_IOS)
+elseif(NOT IOS)
 	target_link_libraries(vcmilauncher SDL2::SDL2)
 endif()
 
@@ -242,17 +245,12 @@ if(ENABLE_INNOEXTRACT)
 	target_link_libraries(vcmilauncher innoextract)
 endif()
 
-if(APPLE_IOS)
+if(IOS)
 	target_link_libraries(vcmilauncher
 		iOS_utils
 		"-framework UniformTypeIdentifiers"
 	)
 
-	# TODO: remove after switching prebuilt deps to a newer Conan's Qt recipe
-	if(XCODE_VERSION VERSION_GREATER_EQUAL 14.0)
-		target_link_libraries(vcmilauncher "-framework IOKit")
-	endif()
-
 	# workaround https://github.com/conan-io/conan-center-index/issues/13332
 	if(USING_CONAN)
 		file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h

File diff suppressed because it is too large
+ 169 - 161
launcher/translation/japanese.ts


+ 7 - 0
lib/CBonusTypeHandler.cpp

@@ -142,6 +142,7 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
 	dest.identifier = name;
 	dest.hidden = source["hidden"].Bool(); //Null -> false
 	dest.creatureNature = source["creatureNature"].Bool(); //Null -> false
+	dest.blockDescriptionPropagation = source["blockDescriptionPropagation"].Bool(); //Null -> false
 
 	if (!dest.hidden)
 		LIBRARY->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"]);
@@ -196,6 +197,12 @@ bool CBonusTypeHandler::isCreatureNatureBonus(BonusType bonus) const
 	return bonusTypes.at(static_cast<int>(bonus))->creatureNature;
 }
 
+bool CBonusTypeHandler::shouldPropagateDescription(BonusType bonus) const
+{
+	return !bonusTypes.at(static_cast<int>(bonus))->blockDescriptionPropagation;
+}
+
+
 std::vector<BonusType> CBonusTypeHandler::getAllObjets() const
 {
 	std::vector<BonusType> ret;

+ 2 - 0
lib/CBonusTypeHandler.h

@@ -38,6 +38,7 @@ private:
 
 	bool creatureNature = false;
 	bool hidden = true;
+	bool blockDescriptionPropagation = false;
 };
 
 class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler
@@ -57,6 +58,7 @@ public:
 	const std::string & bonusToString(BonusType bonus) const;
 
 	bool isCreatureNatureBonus(BonusType bonus) const;
+	bool shouldPropagateDescription(BonusType bonus) const;
 
 	std::vector<BonusType> getAllObjets() const;
 private:

+ 3 - 3
lib/CMakeLists.txt

@@ -847,11 +847,11 @@ endif()
 
 # no longer necessary, but might be useful to keep in future
 # unfortunately at the moment tests do not support namespaced build, so enable only on some systems
-if(APPLE_IOS OR ANDROID)
+if(IOS OR ANDROID)
 	target_compile_definitions(vcmi PUBLIC VCMI_LIB_NAMESPACE=VCMI)
 endif()
 
-if(APPLE_IOS)
+if(IOS)
 	target_link_libraries(vcmi PUBLIC iOS_utils)
 endif()
 
@@ -902,7 +902,7 @@ if(NOT ENABLE_STATIC_LIBS)
 	install(TARGETS vcmi RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR})
 endif()
 
-if(APPLE_IOS AND NOT USING_CONAN)
+if(IOS AND NOT USING_CONAN)
 	install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
 
 	get_target_property(LINKED_LIBS vcmi LINK_LIBRARIES)

+ 12 - 32
lib/CStack.cpp

@@ -246,7 +246,6 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const
 
 BattleHexArray CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos)
 {
-	int mask = 0;
 	BattleHexArray res;
 
 	if (!attackerPos.isValid())
@@ -254,43 +253,24 @@ BattleHexArray CStack::meleeAttackHexes(const battle::Unit * attacker, const bat
 	if (!defenderPos.isValid())
 		defenderPos = defender->getPosition();
 
-	BattleHex otherAttackerPos = attackerPos.toInt() + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1);
-	BattleHex otherDefenderPos = defenderPos.toInt() + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1);
+	BattleHexArray defenderHexes = defender->getHexes(defenderPos);
+	BattleHexArray attackerHexes = attacker->getHexes(attackerPos);
 
-	if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front
+	for (BattleHex defenderHex : defenderHexes)
 	{
-		if((mask & 1) == 0)
+		if (attackerHexes.contains(defenderHex))
 		{
-			mask |= 1;
-			res.insert(defenderPos);
+			logGlobal->debug("CStack::meleeAttackHexes: defender and attacker positions overlap");
+			return res;
 		}
 	}
-	if (attacker->doubleWide() //back <=> front
-		&& BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0)
-	{
-		if((mask & 1) == 0)
-		{
-			mask |= 1;
-			res.insert(defenderPos);
-		}
-	}
-	if (defender->doubleWide()//front <=> back
-		&& BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0)
-	{
-		if((mask & 2) == 0)
-		{
-			mask |= 2;
-			res.insert(otherDefenderPos);
-		}
-	}
-	if (defender->doubleWide() && attacker->doubleWide()//back <=> back
-		&& BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0)
+
+	const BattleHexArray attackableHxs = attacker->getSurroundingHexes(attackerPos);
+
+	for (BattleHex defenderHex : defenderHexes)
 	{
-		if((mask & 2) == 0)
-		{
-			mask |= 2;
-			res.insert(otherDefenderPos);
-		}
+		if (attackableHxs.contains(defenderHex))
+			res.insert(defenderHex);
 	}
 
 	return res;

+ 0 - 6
lib/filesystem/MinizipExtensions.h

@@ -9,15 +9,9 @@
  */
 #pragma once
 
-#ifdef USE_SYSTEM_MINIZIP
 #include <minizip/unzip.h>
 #include <minizip/zip.h>
 #include <minizip/ioapi.h>
-#else
-#include "../minizip/unzip.h"
-#include "../minizip/zip.h"
-#include "../minizip/ioapi.h"
-#endif
 
 // system zlib on old Androids isn't capable of using _64 functions: https://github.com/madler/zlib/pull/436
 #if defined(__ANDROID_API__) && (__ANDROID_API__ < 24)

+ 0 - 22
lib/minizip/CMakeLists.txt

@@ -1,22 +0,0 @@
-project(minizip)
-
-include_directories(${ZLIB_INCLUDE_DIR})
-
-#NOTE: full library consists from several more files
-# but right now VCMI does not need any extra functionality
-set(lib_SRCS
-		unzip.c
-		zip.c
-		ioapi.c
-)
-
-add_library(minizip SHARED ${lib_SRCS})
-if(MSVC)
-    set_target_properties(minizip PROPERTIES COMPILE_DEFINITIONS "MINIZIP_DLL;ZLIB_DLL;ZLIB_INTERNAL")
-endif()
-
-vcmi_set_output_dir(minizip "")
-
-target_link_libraries(minizip ${ZLIB_LIBRARIES})
-
-install(TARGETS minizip RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR})

+ 0 - 6
lib/minizip/MiniZip64_Changes.txt

@@ -1,6 +0,0 @@
-
-MiniZip 1.1 was derived from MiniZip at version 1.01f
-
-Change in 1.0 (Okt 2009)
- - **TODO - Add history**
-

+ 0 - 74
lib/minizip/MiniZip64_info.txt

@@ -1,74 +0,0 @@
-MiniZip - Copyright (c) 1998-2010 - by Gilles Vollant - version 1.1 64 bits from Mathias Svensson
-
-Introduction
----------------------
-MiniZip 1.1 is built from MiniZip 1.0 by Gilles Vollant ( http://www.winimage.com/zLibDll/minizip.html )
-
-When adding ZIP64 support into minizip it would result into risk of breaking compatibility with minizip 1.0.
-All possible work was done for compatibility.
-
-
-Background
----------------------
-When adding ZIP64 support Mathias Svensson found that Even Rouault have added ZIP64 
-support for unzip.c into minizip for a open source project called gdal ( http://www.gdal.org/ )
-
-That was used as a starting point. And after that ZIP64 support was added to zip.c
-some refactoring and code cleanup was also done.
-
-
-Changed from MiniZip 1.0 to MiniZip 1.1
----------------------------------------
-* Added ZIP64 support for unzip ( by Even Rouault )
-* Added ZIP64 support for zip ( by Mathias Svensson )
-* Reverted some changed that Even Rouault did.
-* Bunch of patches received from Gulles Vollant that he received for MiniZip from various users.
-* Added unzip patch for BZIP Compression method (patch create by Daniel Borca)
-* Added BZIP Compress method for zip
-* Did some refactoring and code cleanup
-
-
-Credits
-
- Gilles Vollant    - Original MiniZip author
- Even Rouault      - ZIP64 unzip Support
- Daniel Borca      - BZip Compression method support in unzip
- Mathias Svensson  - ZIP64 zip support
- Mathias Svensson  - BZip Compression method support in zip
-
- Resources
-
- ZipLayout   http://result42.com/projects/ZipFileLayout
-             Command line tool for Windows that shows the layout and information of the headers in a zip archive.
-             Used when debugging and validating the creation of zip files using MiniZip64
-
-
- ZIP App Note  http://www.pkware.com/documents/casestudies/APPNOTE.TXT
-               Zip File specification
-
-
-Notes.
- * To be able to use BZip compression method in zip64.c or unzip64.c the BZIP2 lib is needed and HAVE_BZIP2 need to be defined.
-
-License
-----------------------------------------------------------
-   Condition of use and distribution are the same than zlib :
-
-  This software is provided 'as-is', without any express or implied
-  warranty.  In no event will the authors be held liable for any damages
-  arising from the use of this software.
-
-  Permission is granted to anyone to use this software for any purpose,
-  including commercial applications, and to alter it and redistribute it
-  freely, subject to the following restrictions:
-
-  1. The origin of this software must not be misrepresented; you must not
-     claim that you wrote the original software. If you use this software
-     in a product, an acknowledgment in the product documentation would be
-     appreciated but is not required.
-  2. Altered source versions must be plainly marked as such, and must not be
-     misrepresented as being the original software.
-  3. This notice may not be removed or altered from any source distribution.
-
-----------------------------------------------------------
-

+ 0 - 131
lib/minizip/crypt.h

@@ -1,131 +0,0 @@
-/* crypt.h -- base code for crypt/uncrypt ZIPfile
-
-
-   Version 1.01e, February 12th, 2005
-
-   Copyright (C) 1998-2005 Gilles Vollant
-
-   This code is a modified version of crypting code in Infozip distribution
-
-   The encryption/decryption parts of this source code (as opposed to the
-   non-echoing password parts) were originally written in Europe.  The
-   whole source package can be freely distributed, including from the USA.
-   (Prior to January 2000, re-export from the US was a violation of US law.)
-
-   This encryption code is a direct transcription of the algorithm from
-   Roger Schlafly, described by Phil Katz in the file appnote.txt.  This
-   file (appnote.txt) is distributed with the PKZIP program (even in the
-   version without encryption capabilities).
-
-   If you don't need crypting in your application, just define symbols
-   NOCRYPT and NOUNCRYPT.
-
-   This code support the "Traditional PKWARE Encryption".
-
-   The new AES encryption added on Zip format by Winzip (see the page
-   http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong
-   Encryption is not supported.
-*/
-
-#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8))
-
-/***********************************************************************
- * Return the next byte in the pseudo-random sequence
- */
-static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab)
-{
-    unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an
-                     * unpredictable manner on 16-bit systems; not a problem
-                     * with any known compiler so far, though */
-
-    temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;
-    return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
-}
-
-/***********************************************************************
- * Update the encryption keys with the next byte of plain text
- */
-static int update_keys(unsigned long* pkeys,const z_crc_t* pcrc_32_tab,int c)
-{
-    (*(pkeys+0)) = CRC32((*(pkeys+0)), c);
-    (*(pkeys+1)) += (*(pkeys+0)) & 0xff;
-    (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;
-    {
-      register int keyshift = (int)((*(pkeys+1)) >> 24);
-      (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift);
-    }
-    return c;
-}
-
-
-/***********************************************************************
- * Initialize the encryption keys and the random header according to
- * the given password.
- */
-static void init_keys(const char* passwd,unsigned long* pkeys,const z_crc_t* pcrc_32_tab)
-{
-    *(pkeys+0) = 305419896L;
-    *(pkeys+1) = 591751049L;
-    *(pkeys+2) = 878082192L;
-    while (*passwd != '\0') {
-        update_keys(pkeys,pcrc_32_tab,(int)*passwd);
-        passwd++;
-    }
-}
-
-#define zdecode(pkeys,pcrc_32_tab,c) \
-    (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))
-
-#define zencode(pkeys,pcrc_32_tab,c,t) \
-    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c))
-
-#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED
-
-#define RAND_HEAD_LEN  12
-   /* "last resort" source for second part of crypt seed pattern */
-#  ifndef ZCR_SEED2
-#    define ZCR_SEED2 3141592654UL     /* use PI as default pattern */
-#  endif
-
-static int crypthead(const char* passwd,      /* password string */
-                     unsigned char* buf,      /* where to write header */
-                     int bufSize,
-                     unsigned long* pkeys,
-                     const z_crc_t* pcrc_32_tab,
-                     unsigned long crcForCrypting)
-{
-    int n;                       /* index in random header */
-    int t;                       /* temporary */
-    int c;                       /* random byte */
-    unsigned char header[RAND_HEAD_LEN-2]; /* random header */
-    static unsigned calls = 0;   /* ensure different random header each time */
-
-    if (bufSize<RAND_HEAD_LEN)
-      return 0;
-
-    /* First generate RAND_HEAD_LEN-2 random bytes. We encrypt the
-     * output of rand() to get less predictability, since rand() is
-     * often poorly implemented.
-     */
-    if (++calls == 1)
-    {
-        srand((unsigned)(time(NULL) ^ ZCR_SEED2));
-    }
-    init_keys(passwd, pkeys, pcrc_32_tab);
-    for (n = 0; n < RAND_HEAD_LEN-2; n++)
-    {
-        c = (rand() >> 7) & 0xff;
-        header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t);
-    }
-    /* Encrypt random header (last two bytes is high word of crc) */
-    init_keys(passwd, pkeys, pcrc_32_tab);
-    for (n = 0; n < RAND_HEAD_LEN-2; n++)
-    {
-        buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t);
-    }
-    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);
-    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);
-    return n;
-}
-
-#endif

+ 0 - 247
lib/minizip/ioapi.c

@@ -1,247 +0,0 @@
-/* ioapi.h -- IO base function header for compress/uncompress .zip
-   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Modifications for Zip64 support
-         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
-
-         For more info read MiniZip_info.txt
-
-*/
-
-#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))
-        #define _CRT_SECURE_NO_WARNINGS
-#endif
-
-#if defined(__APPLE__) || defined(IOAPI_NO_64)
-// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
-#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
-#define FTELLO_FUNC(stream) ftello(stream)
-#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)
-#else
-#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
-#define FTELLO_FUNC(stream) ftello64(stream)
-#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
-#endif
-
-
-#include "ioapi.h"
-
-voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)
-{
-    if (pfilefunc->zfile_func64.zopen64_file != NULL)
-        return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
-    else
-    {
-        return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);
-    }
-}
-
-long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)
-{
-    if (pfilefunc->zfile_func64.zseek64_file != NULL)
-        return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
-    else
-    {
-        uLong offsetTruncated = (uLong)offset;
-        if (offsetTruncated != offset)
-            return -1;
-        else
-            return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);
-    }
-}
-
-ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)
-{
-    if (pfilefunc->zfile_func64.zseek64_file != NULL)
-        return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
-    else
-    {
-        uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
-        if ((tell_uLong) == MAXU32)
-            return (ZPOS64_T)-1;
-        else
-            return tell_uLong;
-    }
-}
-
-void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32)
-{
-    p_filefunc64_32->zfile_func64.zopen64_file = NULL;
-    p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
-    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
-    p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
-    p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
-    p_filefunc64_32->zfile_func64.ztell64_file = NULL;
-    p_filefunc64_32->zfile_func64.zseek64_file = NULL;
-    p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;
-    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
-    p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;
-    p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;
-    p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;
-}
-
-
-
-static voidpf  ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode));
-static uLong   ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size));
-static uLong   ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size));
-static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream));
-static long    ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
-static int     ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream));
-static int     ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream));
-
-static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode)
-{
-    FILE* file = NULL;
-    const char* mode_fopen = NULL;
-    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
-        mode_fopen = "rb";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
-        mode_fopen = "r+b";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
-        mode_fopen = "wb";
-
-    if ((filename!=NULL) && (mode_fopen != NULL))
-        file = fopen(filename, mode_fopen);
-    return file;
-}
-
-static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode)
-{
-    FILE* file = NULL;
-    const char* mode_fopen = NULL;
-    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
-        mode_fopen = "rb";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
-        mode_fopen = "r+b";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
-        mode_fopen = "wb";
-
-    if ((filename!=NULL) && (mode_fopen != NULL))
-        file = FOPEN_FUNC((const char*)filename, mode_fopen);
-    return file;
-}
-
-
-static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size)
-{
-    uLong ret;
-    ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
-    return ret;
-}
-
-static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size)
-{
-    uLong ret;
-    ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
-    return ret;
-}
-
-static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream)
-{
-    long ret;
-    ret = ftell((FILE *)stream);
-    return ret;
-}
-
-
-static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream)
-{
-    ZPOS64_T ret;
-    ret = FTELLO_FUNC((FILE *)stream);
-    return ret;
-}
-
-static long ZCALLBACK fseek_file_func (voidpf  opaque, voidpf stream, uLong offset, int origin)
-{
-    int fseek_origin=0;
-    long ret;
-    switch (origin)
-    {
-    case ZLIB_FILEFUNC_SEEK_CUR :
-        fseek_origin = SEEK_CUR;
-        break;
-    case ZLIB_FILEFUNC_SEEK_END :
-        fseek_origin = SEEK_END;
-        break;
-    case ZLIB_FILEFUNC_SEEK_SET :
-        fseek_origin = SEEK_SET;
-        break;
-    default: return -1;
-    }
-    ret = 0;
-    if (fseek((FILE *)stream, offset, fseek_origin) != 0)
-        ret = -1;
-    return ret;
-}
-
-static long ZCALLBACK fseek64_file_func (voidpf  opaque, voidpf stream, ZPOS64_T offset, int origin)
-{
-    int fseek_origin=0;
-    long ret;
-    switch (origin)
-    {
-    case ZLIB_FILEFUNC_SEEK_CUR :
-        fseek_origin = SEEK_CUR;
-        break;
-    case ZLIB_FILEFUNC_SEEK_END :
-        fseek_origin = SEEK_END;
-        break;
-    case ZLIB_FILEFUNC_SEEK_SET :
-        fseek_origin = SEEK_SET;
-        break;
-    default: return -1;
-    }
-    ret = 0;
-
-    if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0)
-                        ret = -1;
-
-    return ret;
-}
-
-
-static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream)
-{
-    int ret;
-    ret = fclose((FILE *)stream);
-    return ret;
-}
-
-static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream)
-{
-    int ret;
-    ret = ferror((FILE *)stream);
-    return ret;
-}
-
-void fill_fopen_filefunc(pzlib_filefunc_def)
-  zlib_filefunc_def* pzlib_filefunc_def;
-{
-    pzlib_filefunc_def->zopen_file = fopen_file_func;
-    pzlib_filefunc_def->zread_file = fread_file_func;
-    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
-    pzlib_filefunc_def->ztell_file = ftell_file_func;
-    pzlib_filefunc_def->zseek_file = fseek_file_func;
-    pzlib_filefunc_def->zclose_file = fclose_file_func;
-    pzlib_filefunc_def->zerror_file = ferror_file_func;
-    pzlib_filefunc_def->opaque = NULL;
-}
-
-void fill_fopen64_filefunc(zlib_filefunc64_def*  pzlib_filefunc_def)
-{
-    pzlib_filefunc_def->zopen64_file = fopen64_file_func;
-    pzlib_filefunc_def->zread_file = fread_file_func;
-    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
-    pzlib_filefunc_def->ztell64_file = ftell64_file_func;
-    pzlib_filefunc_def->zseek64_file = fseek64_file_func;
-    pzlib_filefunc_def->zclose_file = fclose_file_func;
-    pzlib_filefunc_def->zerror_file = ferror_file_func;
-    pzlib_filefunc_def->opaque = NULL;
-}

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