From 932bfdeeb60333d67e338fbac246e08da94eb51c Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Wed, 25 Dec 2024 12:33:01 +0300 Subject: [PATCH 01/20] [android] fix comment about SDL --- .../vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java index 1c7f65cff..393e9ce05 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java +++ b/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[] { }; } From 53fbe2efa50bfd26526d7812346d440f488b9bef Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Wed, 11 Jun 2025 21:35:45 +0300 Subject: [PATCH 02/20] [android] gradle cleanup --- android/vcmi-app/build.gradle | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 62eea479c..49d2bf511 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -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')) From 5028af75cf238b9a9a7c7b43cda69c441c7d05df Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Fri, 7 Mar 2025 11:32:36 +0300 Subject: [PATCH 03/20] [conan] migrate to v2 --- CI/conan/android-32 | 7 + CI/conan/android-32-ndk | 4 +- CI/conan/android-64-ndk | 4 +- CI/conan/base/android | 14 +- CI/conan/base/android-ndk | 2 + CI/conan/base/apple | 16 +- CI/conan/base/common | 14 ++ CI/conan/base/meson_workaround.txt | 2 + CMakeLists.txt | 2 + client/media/CMusicHandler.cpp | 2 +- clientapp/CMakeLists.txt | 3 - cmake_modules/VCMIUtils.cmake | 25 ++- conanfile.py | 285 ++++++++++++++--------------- launcher/CMakeLists.txt | 5 - osx/CMakeLists.txt | 35 +--- 15 files changed, 202 insertions(+), 218 deletions(-) create mode 100644 CI/conan/base/android-ndk create mode 100644 CI/conan/base/common create mode 100644 CI/conan/base/meson_workaround.txt diff --git a/CI/conan/android-32 b/CI/conan/android-32 index 623589d71..61cd8dc7c 100644 --- a/CI/conan/android-32 +++ b/CI/conan/android-32 @@ -3,3 +3,10 @@ include(base/android) [settings] arch=armv7 os.api_level=19 + +[conf] +# remove after switching to API level >= 24 +{% set file_funcs_macros = ['fseeko=fseek', 'ftello=ftell'] %} +flac/*:tools.build:defines={{ file_funcs_macros }} +luajit/*:tools.build:defines={{ file_funcs_macros }} +opusfile/*:tools.build:defines={{ file_funcs_macros }} diff --git a/CI/conan/android-32-ndk b/CI/conan/android-32-ndk index a70c76db0..05d3cd11c 100644 --- a/CI/conan/android-32-ndk +++ b/CI/conan/android-32-ndk @@ -1,4 +1,2 @@ include(android-32) - -[tool_requires] -android-ndk/r25c +include(base/android-ndk) diff --git a/CI/conan/android-64-ndk b/CI/conan/android-64-ndk index e36c5af3b..25caf445e 100644 --- a/CI/conan/android-64-ndk +++ b/CI/conan/android-64-ndk @@ -1,4 +1,2 @@ include(android-64) - -[tool_requires] -android-ndk/r25c +include(base/android-ndk) diff --git a/CI/conan/base/android b/CI/conan/base/android index 507c18bbc..5d18442f4 100644 --- a/CI/conan/base/android +++ b/CI/conan/base/android @@ -1,10 +1,16 @@ +include(common) + [settings] -build_type=Release compiler=clang +compiler.cppstd=17 compiler.libcxx=c++_shared compiler.version=14 os=Android -[buildenv] -# fixes shared libiconv build -LD=ld +[replace_requires] +zlib/*: zlib/[*]@system + +[conf] +# https://github.com/conan-io/conan-center-index/issues/25342 +# https://github.com/conan-io/conan/issues/16468#issuecomment-2175877245 +tools.meson.mesontoolchain:extra_machine_files={{ [os.path.join(profile_dir, "meson_workaround.txt")] }} diff --git a/CI/conan/base/android-ndk b/CI/conan/base/android-ndk new file mode 100644 index 000000000..37e1baed9 --- /dev/null +++ b/CI/conan/base/android-ndk @@ -0,0 +1,2 @@ +[tool_requires] +android-ndk/r25c diff --git a/CI/conan/base/apple b/CI/conan/base/apple index ae85bc702..9fc0917b9 100644 --- a/CI/conan/base/apple +++ b/CI/conan/base/apple @@ -1,12 +1,16 @@ +include(common) + [settings] compiler=apple-clang -compiler.version=14 +compiler.cppstd=17 compiler.libcxx=libc++ -build_type=Release +compiler.version=16 -# required for Boost.Locale in versions >= 1.81 -compiler.cppstd=11 +[replace_requires] +bzip2/*: bzip2/[*]@system +libiconv/*: libiconv/[*]@system +sqlite3/*: sqlite3/[*]@system +zlib/*: zlib/[*]@system [conf] -tools.apple:enable_bitcode = False -tools.cmake.cmaketoolchain:generator = Ninja +tools.apple:enable_bitcode=False diff --git a/CI/conan/base/common b/CI/conan/base/common new file mode 100644 index 000000000..458bbb482 --- /dev/null +++ b/CI/conan/base/common @@ -0,0 +1,14 @@ +[settings] +build_type=Release + +[platform_tool_requires] +cmake/3.31.6 +ninja/1.12.1 + +[replace_requires] +# TODO: remove after https://github.com/conan-io/conan-center-index/pull/27125 is merged +# Qt 5 depends on 0.4.8, but it doesn't build for iOS +md4c/0.4.8: md4c/0.5.2 + +[conf] +tools.cmake.cmaketoolchain:generator=Ninja diff --git a/CI/conan/base/meson_workaround.txt b/CI/conan/base/meson_workaround.txt new file mode 100644 index 000000000..2e2bdae7b --- /dev/null +++ b/CI/conan/base/meson_workaround.txt @@ -0,0 +1,2 @@ +[properties] +sys_root = '' diff --git a/CMakeLists.txt b/CMakeLists.txt index b0f887d5a..c87015c96 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -746,6 +746,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}") diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp index bd9ee81be..245ed2529 100644 --- a/client/media/CMusicHandler.cpp +++ b/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()); diff --git a/clientapp/CMakeLists.txt b/clientapp/CMakeLists.txt index edb0cb873..e82e3b26e 100644 --- a/clientapp/CMakeLists.txt +++ b/clientapp/CMakeLists.txt @@ -90,7 +90,6 @@ vcmi_set_output_dir(vcmiclient "") enable_pch(vcmiclient) if(APPLE_IOS) - vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}") add_custom_command(TARGET vcmiclient POST_BUILD COMMAND ios/set_build_version.sh "$" COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$" --prefix "$" @@ -103,8 +102,6 @@ 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 "$" --prefix "${androidQtBuildDir}" COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$>:--release> ${ANDROIDDEPLOYQT_OPTIONS} diff --git a/cmake_modules/VCMIUtils.cmake b/cmake_modules/VCMIUtils.cmake index ca0726fe4..d13bd4496 100644 --- a/cmake_modules/VCMIUtils.cmake +++ b/cmake_modules/VCMIUtils.cmake @@ -128,15 +128,24 @@ function(install_vcpkg_imported_tgt tgt) 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() diff --git a/conanfile.py b/conanfile.py index 302aba724..7a79d4de2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,92 +1,64 @@ 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 +from conan.tools.cmake import CMakeToolchain +from conan.tools.files import save +from conan.tools.microsoft import is_msvc -required_conan_version = ">=1.51.3" +from glob import glob +import os + +required_conan_version = ">=2.13.0" class VCMI(ConanFile): settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps" _libRequires = [ "boost/[^1.69]", - "minizip/[~1.2.12]", + "luajit/2.1.0-beta3", + "minizip/[^1.2.12]", + "zlib/[^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 + "onetbb/[^2021.7]", + "sdl_image/[^2.8.2]", + "sdl_mixer/[^2.8.0]", + "sdl_ttf/[^2.0.18]", ] - - requires = _libRequires + _clientRequires + _launcherRequires = [ + "xz_utils/[^5.2.5]", # innoextract + ] + requires = _libRequires + _clientRequires + _launcherRequires 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, + "bzip2/*:shared": True, + "libiconv/*:shared": True, + "libpng/*:shared": True, "minizip/*:shared": True, + "ogg/*:shared": True, + "opus/*:shared": True, + "xz_utils/*:shared": True, + "zlib/*: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" + self.options["ffmpeg"].shared = is_msvc(self) # MSVC static build requires static runtime, but VCMI uses dynamic runtime + # self.options["freetype"].shared = self.settings.os == "Android" # TODO https://github.com/conan-io/conan-center-index/issues/26020 - # 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 + self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") # , default="" # we need only the following Boost parts: - # date_time filesystem iostreams locale program_options system thread + # date_time filesystem iostreams locale program_options system # 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 @@ -100,6 +72,7 @@ class VCMI(ConanFile): self.options["boost"].without_math = True self.options["boost"].without_mpi = True self.options["boost"].without_nowide = True + self.options["boost"].without_process = True self.options["boost"].without_python = True self.options["boost"].without_serialization = True self.options["boost"].without_stacktrace = True @@ -120,7 +93,6 @@ class VCMI(ConanFile): 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 @@ -134,12 +106,11 @@ class VCMI(ConanFile): 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 + # option not available on Android if self.settings.os != "Android": self.options["ffmpeg"].with_libfdk_aac = False @@ -157,9 +128,32 @@ class VCMI(ConanFile): # 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" + self.options["ffmpeg"].enable_demuxers = ",".join([ + "bink", + "binka", + "ogg", + "smacker", + "webm_dash_manifest", + ]) + self.options["ffmpeg"].enable_parsers = ",".join([ + "opus", + "vorbis", + "vp8", + "vp9", + "webp", + ]) + self.options["ffmpeg"].enable_decoders = ",".join([ + "bink", + "binkaudio_dct", + "binkaudio_rdft", + "opus", + "smackaud", + "smacker", + "theora", + "vorbis", + "vp8", + "vp9", + ]) #optionally, for testing - enable ffplay/ffprobe binaries in conan package: #if self.settings.os == "Windows": @@ -168,6 +162,11 @@ class VCMI(ConanFile): # self.options["ffmpeg"].with_sdl = True # self.options["ffmpeg"].enable_filters = "aresample,scale" + self.options["onetbb"].tbbbind = False + self.options["onetbb"].tbbmalloc = False + self.options["onetbb"].tbbproxy = False + + self.options["sdl"].iconv = True self.options["sdl"].sdl2main = self.settings.os != "iOS" self.options["sdl"].vulkan = False @@ -177,7 +176,7 @@ class VCMI(ConanFile): 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"].qoi = False self.options["sdl_image"].svg = False self.options["sdl_image"].tga = False self.options["sdl_image"].with_libjpeg = False @@ -189,17 +188,19 @@ class VCMI(ConanFile): 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 + # mp3, ogg and wav are the only ones that needs to be supported, flac is a bonus + self.options["sdl_mixer"].mad = False + self.options["sdl_mixer"].mikmod = 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 + self.options["sdl_mixer"].nativemidi = False + self.options["sdl_mixer"].tinymidi = False + + # static on "single app" platforms + isSdlShared = self.settings.os != "iOS" and self.settings.os != "Android" + self.options["sdl"].shared = isSdlShared + self.options["sdl_image"].shared = isSdlShared + self.options["sdl_mixer"].shared = isSdlShared + self.options["sdl_ttf"].shared = isSdlShared def _disableQtOptions(disableFlag, options): return " ".join([f"-{disableFlag}-{tool}" for tool in options]) @@ -216,7 +217,7 @@ class VCMI(ConanFile): "imageformat_ppm", "imageformat_xbm", - # we need only macdeployqt + # we need only win/macdeployqt # TODO: disabling these doesn't disable generation of CMake targets # TODO: in Qt 6.3 it's a part of qtbase # "assistant", @@ -235,11 +236,11 @@ class VCMI(ConanFile): ]), ] self.options["qt"].config = " ".join(_qtOptions) + self.options["qt"].essential_modules = False 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 @@ -247,105 +248,85 @@ class VCMI(ConanFile): 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 + self.options["sqlite3"].build_executable = False + # prevents pulling openssl in and isn't needed anyway + self.options["opusfile"].http = False + # programs not needed + self.options["zstd"].build_programs = 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]") + # On Android SDL version must be the same as the version of Java wrapper for SDL in VCMI source code + # Wrapper can be found in the following directory: android/vcmi-app/src/main/java/org/libsdl/app + # TODO: try enabling version range once there's no conflict + # sdl_image & sdl_ttf depend on earlier version + # ERROR: Version conflict: Conflict between sdl/2.28.5 and sdl/2.28.3 in the graph. + # Conflict originates from sdl_mixer/2.8.0 + # 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.1 || >=2.0.20 <=2.22.0]") + # versions before 2.30.7 don't build for Android with NDK 27: https://github.com/libsdl-org/SDL/issues/9792 + self.requires("sdl/2.30.9", override=True) + # launcher if self.settings.os == "Android": - self.requires("qt/[~5.15.14]") + self.requires("qt/[~5.15.14]") # earlier versions have serious bugs 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 + def _pathForCmake(self, path): + # CMake doesn't like \ in strings + return path.replace(os.path.sep, os.path.altsep) if os.path.altsep else path - # 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 _generateRuntimeLibsFile(self): + # 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")) - 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") + 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 += 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', + "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") 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() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 065596946..b8a5c7c03 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -248,11 +248,6 @@ if(APPLE_IOS) "-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 diff --git a/osx/CMakeLists.txt b/osx/CMakeLists.txt index 334860827..4230553cb 100644 --- a/osx/CMakeLists.txt +++ b/osx/CMakeLists.txt @@ -5,41 +5,10 @@ if(APPLE_MACOS) set(bundleContentsDir "${bundleDir}/Contents") if(ENABLE_LAUNCHER OR ENABLE_EDITOR) - if(USING_CONAN) - # simulate macdeployqt behavior, main Qt libs are copied by conan - execute_process(COMMAND - "${qmakePath}" -query QT_INSTALL_PLUGINS - OUTPUT_VARIABLE qtPluginsDir - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - install(DIRECTORY - ${qtPluginsDir}/ - DESTINATION ${APP_BUNDLE_DIR}/Contents/PlugIns - ) - install(CODE " - file(WRITE ${bundleContentsDir}/Resources/qt.conf - \"[Paths]\nPlugins = PlugIns\" - ) - ") - else() - # note: cross-compiled Qt 5 builds macdeployqt for target platform instead of host - # deploy Qt dylibs with macdeployqt - find_program(TOOL_MACDEPLOYQT NAMES macdeployqt PATHS "${qtBinDir}") - if(TOOL_MACDEPLOYQT) - install(CODE " - execute_process(COMMAND - \"${TOOL_MACDEPLOYQT}\" \"${bundleDir}\" -verbose=2 - ) - ") - else() - message(WARNING "macdeployqt not found, running cpack would result in broken package") - endif() - endif() + # note: cross-compiled Qt 5 builds macdeployqt for target platform instead of host + vcmi_deploy_qt(macdeployqt "\"${bundleDir}\"") endif() - # deploy other dylibs with conan - vcmi_install_conan_deps("${bundleContentsDir}") - # perform ad-hoc codesigning # Intel Macs don't need it if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") From 5e2355dde2ebf4a0058c50812a33c90b348dce2e Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sun, 17 Aug 2025 11:25:57 +0300 Subject: [PATCH 04/20] [cmake][launcher] innoextract: disable libiconv on Android it finds it in the toolchain but that one is usable only since API level 28 --- launcher/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b8a5c7c03..bf01c43bd 100644 --- a/launcher/CMakeLists.txt +++ b/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() From 951be24f5e4a61a34b88cc51fbe28a0b8af3946f Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sun, 9 Mar 2025 13:41:27 +0300 Subject: [PATCH 05/20] [cmake][android] improve fetching ANDROID_SYSROOT_LIB_SUBDIR now it uses info from file bundled in the NDK --- CMakeLists.txt | 3 +++ conanfile.py | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c87015c96..9ec500fc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -626,6 +626,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") diff --git a/conanfile.py b/conanfile.py index 7a79d4de2..fe0601e9c 100644 --- a/conanfile.py +++ b/conanfile.py @@ -321,12 +321,6 @@ class VCMI(ConanFile): 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)) elif self.settings.os == "Windows": tc.variables["CONAN_RUNENV_SCRIPT"] = self._pathForCmake(os.path.join(self.build_folder, "conanrun.bat")) tc.generate() From a3f08c2217f01c79958f2155262989b816d5c363 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Fri, 7 Mar 2025 11:36:08 +0300 Subject: [PATCH 06/20] [conan] add MSVC support also removes all mentions of vcpkg and cross-building from Linux --- AI/CMakeLists.txt | 4 --- CI/conan/base/msvc | 21 ++++++++++++ CI/conan/msvc-x64 | 4 +++ CI/conan/msvc-x86 | 4 +++ CMakeLists.txt | 60 +---------------------------------- clientapp/CMakeLists.txt | 10 +----- cmake_modules/VCMIUtils.cmake | 21 ++++++------ launcher/CMakeLists.txt | 1 + mapeditor/CMakeLists.txt | 1 + serverapp/CMakeLists.txt | 1 + win/CMakeLists.txt | 49 +++++----------------------- 11 files changed, 53 insertions(+), 123 deletions(-) create mode 100644 CI/conan/base/msvc create mode 100644 CI/conan/msvc-x64 create mode 100644 CI/conan/msvc-x86 diff --git a/AI/CMakeLists.txt b/AI/CMakeLists.txt index 796b2b10d..b309acc0e 100644 --- a/AI/CMakeLists.txt +++ b/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 "") diff --git a/CI/conan/base/msvc b/CI/conan/base/msvc new file mode 100644 index 000000000..9a63268e1 --- /dev/null +++ b/CI/conan/base/msvc @@ -0,0 +1,21 @@ +include(common) + +[settings] +compiler=msvc +compiler.cppstd=17 +compiler.runtime=dynamic +compiler.version=194 +os=Windows + +[conf] +# https://walbourn.github.io/a-brief-history-of-windows-sdks/ +# https://learn.microsoft.com/en-us/cpp/porting/modifying-winver-and-win32-winnt +# https://learn.microsoft.com/en-us/windows/win32/WinProg/using-the-windows-headers +{% set _WIN32_WINNT_WIN7 = '0x0601' %} +{% set NTDDI_WIN7 = '0x06010000' %} +{% set win7_defines = [ + '_WIN32_WINNT={}'.format(_WIN32_WINNT_WIN7), + 'WINVER={}'.format(_WIN32_WINNT_WIN7), + 'NTDDI_VERSION={}'.format(NTDDI_WIN7), +] %} +tools.build:defines={{ win7_defines }} diff --git a/CI/conan/msvc-x64 b/CI/conan/msvc-x64 new file mode 100644 index 000000000..9f2334126 --- /dev/null +++ b/CI/conan/msvc-x64 @@ -0,0 +1,4 @@ +include(base/msvc) + +[settings] +arch=x86_64 diff --git a/CI/conan/msvc-x86 b/CI/conan/msvc-x86 new file mode 100644 index 000000000..b70e318d9 --- /dev/null +++ b/CI/conan/msvc-x86 @@ -0,0 +1,4 @@ +include(base/msvc) + +[settings] +arch=x86 diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ec500fc5..8e6d1efb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,7 +310,7 @@ 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 +373,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) @@ -779,59 +774,6 @@ if(NOT WIN32 AND NOT APPLE_IOS AND NOT ANDROID) 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 "$" - DESTINATION ${BIN_DIR}/styles) - endif() - endif() - - install(FILES ${dep_files} DESTINATION ${BIN_DIR}) -endif(WIN32) - ####################################### # Packaging section # ####################################### diff --git a/clientapp/CMakeLists.txt b/clientapp/CMakeLists.txt index e82e3b26e..a9f60b744 100644 --- a/clientapp/CMakeLists.txt +++ b/clientapp/CMakeLists.txt @@ -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,15 +56,6 @@ 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 "$" - COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll - COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll - ) - endif() elseif(APPLE_IOS) set_target_properties(vcmiclient PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist" diff --git a/cmake_modules/VCMIUtils.cmake b/cmake_modules/VCMIUtils.cmake index d13bd4496..443f916f1 100644 --- a/cmake_modules/VCMIUtils.cmake +++ b/cmake_modules/VCMIUtils.cmake @@ -118,16 +118,6 @@ 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, CONAN_RUNTIME_LIBS_FILE is set in conanfile.py function(vcmi_install_conan_deps) if(NOT USING_CONAN) @@ -149,3 +139,14 @@ function(vcmi_deploy_qt deployQtToolName deployQtOptions) 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 "$/$.bat" CONTENT +"call ${CONAN_RUNENV_SCRIPT} +@start $" + ) +endfunction() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index bf01c43bd..95dab7cec 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -155,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) diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 44e514dc6..4ae5a0445 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -258,6 +258,7 @@ if(ENABLE_SINGLE_APP_BUILD OR ANDROID) add_library(vcmieditor OBJECT ${editor_QM}) else() add_executable(vcmieditor WIN32 ${editor_QM} ${editor_SRCS} ${editor_HEADERS} ${editor_UI_HEADERS} ${editor_ICON}) + vcmi_create_exe_shim(vcmieditor) endif() if(ENABLE_TRANSLATIONS) diff --git a/serverapp/CMakeLists.txt b/serverapp/CMakeLists.txt index 79af9d73f..7d3a2cb96 100644 --- a/serverapp/CMakeLists.txt +++ b/serverapp/CMakeLists.txt @@ -9,6 +9,7 @@ set(serverapp_HEADERS assign_source_group(${serverapp_SRCS} ${serverapp_HEADERS}) add_executable(vcmiserver ${serverapp_SRCS} ${serverapp_HEADERS}) +vcmi_create_exe_shim(vcmiserver) set(serverapp_LIBS vcmi) if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU) diff --git a/win/CMakeLists.txt b/win/CMakeLists.txt index bf3ab9922..8049dfc94 100644 --- a/win/CMakeLists.txt +++ b/win/CMakeLists.txt @@ -1,43 +1,10 @@ # We need to keep this code into separate directory so CMake will execute it after all other subdirectories install code -# Otherwise we can't fix win bundle dependencies since binaries wouldn't be there when this code executed -# This will likely only work for Vcpkg -if(WIN32) - #there are some weird issues with variables used in path not evaluated properly when trying to remove code duplication from below lines - - if(ENABLE_LAUNCHER) - install(CODE " - file(WRITE \"\${CMAKE_INSTALL_PREFIX}/qt.conf\" - \"[Paths]\nPlugins = .\" - ) - ") +if(WIN32 AND (ENABLE_LAUNCHER OR ENABLE_EDITOR)) + if(TARGET vcmilauncher) + set(deployTarget vcmilauncher) + elseif(TARGET vcmieditor) + set(deployTarget vcmieditor) endif() - - - #TODO: check if some equivalent of block below can be used for above block (easy qt dependencies copy) - #LuaJIT will not be copied automatically by not meeting criteria for this block of code - if(ENABLE_LUA) - install_vcpkg_imported_tgt(luajit::luajit) - endif() - - if(MSVC) - set(gp_tool "dumpbin") - endif() - - install(CODE " - set(dirs \"${CMAKE_PREFIX_PATH}\") - if(\"\${CMAKE_INSTALL_CONFIG_NAME}\" STREQUAL \"Debug\") - list(TRANSFORM dirs APPEND \"/debug/bin\") - else() - list(TRANSFORM dirs APPEND \"/bin\") - list(FILTER dirs EXCLUDE REGEX \".*debug.*\") - endif() - - set(BU_CHMOD_BUNDLE_ITEMS ON) - set(gp_tool \"${gp_tool}\") - - include(BundleUtilities) - - fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/VCMI_Client.exe\" \"\" \"\${dirs}\") - - " COMPONENT Runtime) -endif(WIN32) + set(targetBinary "\"\${CMAKE_INSTALL_PREFIX}/${BIN_DIR}/$\"") + vcmi_deploy_qt(windeployqt "--no-compiler-runtime ${targetBinary}") +endif() From 9d4fc3f89bd608f8f22ccf2aabedc0d99e16e832 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sun, 9 Mar 2025 15:14:43 +0300 Subject: [PATCH 07/20] [conan] remove Linux MinGW profiles --- CI/conan/base/cross-macro.j2 | 21 --------------------- CI/conan/base/cross-windows | 10 ---------- CI/conan/mingw32-linux.jinja | 15 --------------- CI/conan/mingw64-linux.jinja | 13 ------------- 4 files changed, 59 deletions(-) delete mode 100644 CI/conan/base/cross-macro.j2 delete mode 100644 CI/conan/base/cross-windows delete mode 100644 CI/conan/mingw32-linux.jinja delete mode 100644 CI/conan/mingw64-linux.jinja diff --git a/CI/conan/base/cross-macro.j2 b/CI/conan/base/cross-macro.j2 deleted file mode 100644 index ba3c53212..000000000 --- a/CI/conan/base/cross-macro.j2 +++ /dev/null @@ -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 -%} \ No newline at end of file diff --git a/CI/conan/base/cross-windows b/CI/conan/base/cross-windows deleted file mode 100644 index 5c3c55115..000000000 --- a/CI/conan/base/cross-windows +++ /dev/null @@ -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 diff --git a/CI/conan/mingw32-linux.jinja b/CI/conan/mingw32-linux.jinja deleted file mode 100644 index 7173a0969..000000000 --- a/CI/conan/mingw32-linux.jinja +++ /dev/null @@ -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) }} \ No newline at end of file diff --git a/CI/conan/mingw64-linux.jinja b/CI/conan/mingw64-linux.jinja deleted file mode 100644 index 4c6b59ef0..000000000 --- a/CI/conan/mingw64-linux.jinja +++ /dev/null @@ -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) }} \ No newline at end of file From 3ee4f6650b32c3cfddc0215b5d92d934a528a2d5 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Tue, 11 Mar 2025 14:20:00 +0300 Subject: [PATCH 08/20] [conan] move options to profiles --- CI/conan/base/android | 12 +++ CI/conan/base/apple | 4 + CI/conan/base/common | 209 +++++++++++++++++++++++++++++++++++++++ CI/conan/base/ios | 10 ++ CI/conan/base/msvc | 3 + conanfile.py | 224 ++++-------------------------------------- 6 files changed, 255 insertions(+), 207 deletions(-) diff --git a/CI/conan/base/android b/CI/conan/base/android index 5d18442f4..d5f574b18 100644 --- a/CI/conan/base/android +++ b/CI/conan/base/android @@ -14,3 +14,15 @@ zlib/*: zlib/[*]@system # https://github.com/conan-io/conan-center-index/issues/25342 # https://github.com/conan-io/conan/issues/16468#issuecomment-2175877245 tools.meson.mesontoolchain:extra_machine_files={{ [os.path.join(profile_dir, "meson_workaround.txt")] }} + +[options] +# TODO https://github.com/conan-io/conan-center-index/issues/26020 +# freetype/*:shared=True +sdl/*:shared=False +sdl_image/*:shared=False +sdl_mixer/*:shared=False +sdl_ttf/*:shared=False + +qt/*:opengl=es2 +qt/*:qtandroidextras=True +qt/*:with_freetype=True diff --git a/CI/conan/base/apple b/CI/conan/base/apple index 9fc0917b9..27ba8394f 100644 --- a/CI/conan/base/apple +++ b/CI/conan/base/apple @@ -14,3 +14,7 @@ zlib/*: zlib/[*]@system [conf] tools.apple:enable_bitcode=False + +[options] +qt/*:openssl=False +sdl_image/*:imageio=True diff --git a/CI/conan/base/common b/CI/conan/base/common index 458bbb482..cf151e0a9 100644 --- a/CI/conan/base/common +++ b/CI/conan/base/common @@ -12,3 +12,212 @@ md4c/0.4.8: md4c/0.5.2 [conf] tools.cmake.cmaketoolchain:generator=Ninja + +[options] +# shared (dynamic) libs +boost/*:shared=True +bzip2/*:shared=True +libiconv/*:shared=True +libpng/*:shared=True +minizip/*:shared=True +ogg/*:shared=True +opus/*:shared=True +qt/*:shared=True +sdl/*:shared=True +sdl_image/*:shared=True +sdl_mixer/*:shared=True +sdl_ttf/*:shared=True +xz_utils/*:shared=True +zlib/*:shared=True + +# Boost +# we need only the following parts: +# date_time filesystem iostreams locale program_options system +# some other parts are also enabled because they're dependents +# see e.g. conan-center-index/recipes/boost/all/dependencies +boost/*:without_context=True +boost/*:without_contract=True +boost/*:without_coroutine=True +boost/*:without_fiber=True +boost/*:without_graph=True +boost/*:without_graph_parallel=True +boost/*:without_json=True +boost/*:without_log=True +boost/*:without_math=True +boost/*:without_mpi=True +boost/*:without_nowide=True +boost/*:without_process=True +boost/*:without_python=True +boost/*:without_serialization=True +boost/*:without_stacktrace=True +boost/*:without_test=True +boost/*:without_timer=True +boost/*:without_type_erasure=True +boost/*:without_wave=True +boost/*:without_url=True + +# FFmpeg +ffmpeg/*:avcodec=True +ffmpeg/*:avdevice=False +ffmpeg/*:avfilter=False +ffmpeg/*:avformat=True +ffmpeg/*:postproc=False +# for resampling of audio in 'planar' formats +ffmpeg/*:swresample=True +# for video scaling +ffmpeg/*:swscale=True + +ffmpeg/*:disable_all_bitstream_filters=True +ffmpeg/*:disable_all_decoders=True +ffmpeg/*:disable_all_demuxers=True +ffmpeg/*:disable_all_encoders=True +ffmpeg/*:disable_all_filters=True +ffmpeg/*:disable_all_hardware_accelerators=True +ffmpeg/*:disable_all_muxers=True +ffmpeg/*:disable_all_parsers=True +ffmpeg/*:disable_all_protocols=True + +ffmpeg/*:with_asm=False +ffmpeg/*:with_freetype=False +ffmpeg/*:with_libaom=False +ffmpeg/*:with_libdav1d=False +ffmpeg/*:with_libfdk_aac=False +ffmpeg/*:with_libiconv=False +ffmpeg/*:with_libmp3lame=False +ffmpeg/*:with_libsvtav1=False +ffmpeg/*:with_libvpx=False +ffmpeg/*:with_libwebp=False +ffmpeg/*:with_libx264=False +ffmpeg/*:with_libx265=False +ffmpeg/*:with_lzma=True +ffmpeg/*:with_openh264=False +ffmpeg/*:with_openjpeg=False +ffmpeg/*:with_programs=False +ffmpeg/*:with_sdl=False +ffmpeg/*:with_ssl=False +ffmpeg/*:with_vorbis=False + +# 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) +{% set ffDecoders = [ + 'bink', + 'binkaudio_dct', + 'binkaudio_rdft', + 'opus', + 'smackaud', + 'smacker', + 'theora', + 'vorbis', + 'vp8', + 'vp9', +] %} +{% set ffDemuxers = [ + 'bink', + 'binka', + 'ogg', + 'smacker', + 'webm_dash_manifest', +] %} +{% set ffParsers = [ + 'opus', + 'vorbis', + 'vp8', + 'vp9', + 'webp', +] %} +{% set ffProtocols = [ + 'file', +] %} +ffmpeg/*:enable_decoders={{ ffDecoders|join(',') }} +ffmpeg/*:enable_demuxers={{ ffDemuxers|join(',') }} +ffmpeg/*:enable_parsers={{ ffParsers|join(',') }} +ffmpeg/*:enable_protocols={{ ffProtocols|join(',') }} + +# optionally, for testing - enable ffplay/ffprobe binaries: +# ffmpeg/*:avfilter=True +# ffmpeg/*:enable_filters=aresample,scale +# ffmpeg/*:with_programs=True +# ffmpeg/*:with_sdl=True + +# oneTBB +onetbb/*:tbbbind=False +onetbb/*:tbbmalloc=False +onetbb/*:tbbproxy=False + +# Qt +# xpm format is required for Drag'n'Drop support +# we need only win/macdeployqt +# TODO: disabling these doesn't disable generation of CMake targets +# TODO: in Qt 6.3 it's a part of qtbase +# '-no-feature-assistant', +# '-no-feature-designer', +# '-no-feature-distancefieldgenerator', +# '-no-feature-kmap2qmap', +# '-no-feature-linguist', +# '-no-feature-makeqpf', +# '-no-feature-pixeltool', +# '-no-feature-qdbus', +# '-no-feature-qev', +# '-no-feature-qtattributionsscanner', +# '-no-feature-qtdiag', +# '-no-feature-qtpaths', +# '-no-feature-qtplugininfo', +{% set qtConfig = [ + '-no-gif', + '-no-ico', + + '-no-feature-imageformat_bmp', + '-no-feature-imageformat_jpeg', + '-no-feature-imageformat_ppm', + '-no-feature-imageformat_xbm', +] %} +qt/*:config={{ qtConfig|join(' ') }} +qt/*:essential_modules=False +qt/*:openssl=True +qt/*:qttools=True +qt/*:with_freetype=False +qt/*:with_libjpeg=False +qt/*:with_mysql=False +qt/*:with_odbc=False +qt/*:with_openal=False +qt/*:with_pq=False + +# SDL +sdl/*:iconv=True +sdl/*:sdl2main=True +sdl/*:vulkan=False + +# SDL_image +# 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 +sdl_image/*:gif=False +sdl_image/*:lbm=False +sdl_image/*:pcx=False +sdl_image/*:pnm=False +sdl_image/*:qoi=False +sdl_image/*:svg=False +sdl_image/*:tga=False +sdl_image/*:with_libjpeg=False +sdl_image/*:with_libtiff=False +sdl_image/*:with_libwebp=False +sdl_image/*:xcf=False +sdl_image/*:xpm=False +sdl_image/*:xv=False + +# SDL_mixer +# mp3, ogg and wav are the only ones that needs to be supported, flac is a bonus +sdl_mixer/*:mad=False +sdl_mixer/*:mikmod=False +sdl_mixer/*:modplug=False +sdl_mixer/*:nativemidi=False +sdl_mixer/*:tinymidi=False + +# transitive deps, stuff that's not needed +# doesn't link to bzip2 & zlib from "system" recipes +pcre2/*:build_pcre2grep=False +sqlite3/*:build_executable=False +opusfile/*:http=False +zstd/*:build_programs=False diff --git a/CI/conan/base/ios b/CI/conan/base/ios index 0f48cadf2..1f2230abf 100644 --- a/CI/conan/base/ios +++ b/CI/conan/base/ios @@ -3,3 +3,13 @@ include(apple) [settings] os=iOS os.sdk=iphoneos + +[options] +qt/*:shared=False +sdl/*:shared=False +sdl_image/*:shared=False +sdl_mixer/*:shared=False +sdl_ttf/*:shared=False + +qt/*:opengl=es2 +sdl/*:sdl2main=False diff --git a/CI/conan/base/msvc b/CI/conan/base/msvc index 9a63268e1..df4dcf33c 100644 --- a/CI/conan/base/msvc +++ b/CI/conan/base/msvc @@ -19,3 +19,6 @@ os=Windows 'NTDDI_VERSION={}'.format(NTDDI_WIN7), ] %} tools.build:defines={{ win7_defines }} + +[options] +ffmpeg/*:shared=True diff --git a/conanfile.py b/conanfile.py index fe0601e9c..faf20bdd9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,229 +35,39 @@ class VCMI(ConanFile): } default_options = { "with_ffmpeg": True, - - "boost/*:shared": True, - "bzip2/*:shared": True, - "libiconv/*:shared": True, - "libpng/*:shared": True, - "minizip/*:shared": True, - "ogg/*:shared": True, - "opus/*:shared": True, - "xz_utils/*:shared": True, - "zlib/*:shared": True, } - def configure(self): - self.options["ffmpeg"].shared = is_msvc(self) # MSVC static build requires static runtime, but VCMI uses dynamic runtime - # self.options["freetype"].shared = self.settings.os == "Android" # TODO https://github.com/conan-io/conan-center-index/issues/26020 - - # static Qt for iOS is the only viable option at the moment - self.options["qt"].shared = self.settings.os != "iOS" - - if self.settings.os == "Android": - self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") # , default="" - - # we need only the following Boost parts: - # date_time filesystem iostreams locale program_options system - # 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_process = 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_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_programs = False - self.options["ffmpeg"].with_sdl = False - self.options["ffmpeg"].with_ssl = False - self.options["ffmpeg"].with_vorbis = False - # option not available on Android - 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 = ",".join([ - "bink", - "binka", - "ogg", - "smacker", - "webm_dash_manifest", - ]) - self.options["ffmpeg"].enable_parsers = ",".join([ - "opus", - "vorbis", - "vp8", - "vp9", - "webp", - ]) - self.options["ffmpeg"].enable_decoders = ",".join([ - "bink", - "binkaudio_dct", - "binkaudio_rdft", - "opus", - "smackaud", - "smacker", - "theora", - "vorbis", - "vp8", - "vp9", - ]) - - #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["onetbb"].tbbbind = False - self.options["onetbb"].tbbmalloc = False - self.options["onetbb"].tbbproxy = False - - self.options["sdl"].iconv = True - 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 - 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, flac is a bonus - self.options["sdl_mixer"].mad = False - self.options["sdl_mixer"].mikmod = False - self.options["sdl_mixer"].modplug = False - self.options["sdl_mixer"].nativemidi = False - self.options["sdl_mixer"].tinymidi = False + @property + def _isMobilePlatform(self): + return self.settings.os == "iOS" or self.settings.os == "Android" + def config_options(self): # static on "single app" platforms - isSdlShared = self.settings.os != "iOS" and self.settings.os != "Android" + isSdlShared = not self._isMobilePlatform self.options["sdl"].shared = isSdlShared self.options["sdl_image"].shared = isSdlShared self.options["sdl_mixer"].shared = isSdlShared self.options["sdl_ttf"].shared = isSdlShared - def _disableQtOptions(disableFlag, options): - return " ".join([f"-{disableFlag}-{tool}" for tool in options]) + if self.settings.os == "Android": + self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") - _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", + def configure(self): + self.options["sdl"].sdl2main = self.settings.os != "iOS" - # we need only win/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"].essential_modules = False 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_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": + if self._isMobilePlatform: self.options["qt"].opengl = "es2" - # transitive deps - # doesn't link to overridden bzip2 & zlib, the tool isn't needed anyway - self.options["pcre2"].build_pcre2grep = False - # executable not needed - self.options["sqlite3"].build_executable = False - # prevents pulling openssl in and isn't needed anyway - self.options["opusfile"].http = False - # programs not needed - self.options["zstd"].build_programs = False + # static Qt for iOS is the only viable option at the moment + if self.settings.os == "iOS": + self.options["qt"].shared = False + + # MSVC static build requires static runtime, but VCMI uses dynamic runtime + if is_msvc(self): + self.options["ffmpeg"].shared = True def requirements(self): # client From 3e9e47077ded4cbbe5310edaaab78977c86f7ae4 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Fri, 14 Mar 2025 22:08:07 +0300 Subject: [PATCH 09/20] [conan] share C++ standard declaration --- CI/conan/base/android | 4 +++- CI/conan/base/apple | 4 +++- CI/conan/base/msvc | 4 +++- CI/conan/base/vars.jinja | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 CI/conan/base/vars.jinja diff --git a/CI/conan/base/android b/CI/conan/base/android index d5f574b18..a791b7df5 100644 --- a/CI/conan/base/android +++ b/CI/conan/base/android @@ -1,8 +1,10 @@ +{% import 'vars.jinja' as vars %} + include(common) [settings] compiler=clang -compiler.cppstd=17 +compiler.cppstd={{ vars.cppstd }} compiler.libcxx=c++_shared compiler.version=14 os=Android diff --git a/CI/conan/base/apple b/CI/conan/base/apple index 27ba8394f..ee16f30c0 100644 --- a/CI/conan/base/apple +++ b/CI/conan/base/apple @@ -1,8 +1,10 @@ +{% import 'vars.jinja' as vars %} + include(common) [settings] compiler=apple-clang -compiler.cppstd=17 +compiler.cppstd={{ vars.cppstd }} compiler.libcxx=libc++ compiler.version=16 diff --git a/CI/conan/base/msvc b/CI/conan/base/msvc index df4dcf33c..6ef76c828 100644 --- a/CI/conan/base/msvc +++ b/CI/conan/base/msvc @@ -1,8 +1,10 @@ +{% import 'vars.jinja' as vars %} + include(common) [settings] compiler=msvc -compiler.cppstd=17 +compiler.cppstd={{ vars.cppstd }} compiler.runtime=dynamic compiler.version=194 os=Windows diff --git a/CI/conan/base/vars.jinja b/CI/conan/base/vars.jinja new file mode 100644 index 000000000..cfa2d90c7 --- /dev/null +++ b/CI/conan/base/vars.jinja @@ -0,0 +1 @@ +{% set cppstd = '17' %} From c376507dac5fe4af9c59fe694160b2dee8b70539 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sat, 15 Mar 2025 20:49:00 +0300 Subject: [PATCH 10/20] [conan] raise exception for invalid dependencies' options --- conanfile.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/conanfile.py b/conanfile.py index faf20bdd9..b18d21062 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,5 @@ from conan import ConanFile +from conan.errors import ConanInvalidConfiguration from conan.tools.apple import is_apple_os from conan.tools.cmake import CMakeToolchain from conan.tools.files import save @@ -52,23 +53,6 @@ class VCMI(ConanFile): if self.settings.os == "Android": self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") - def configure(self): - self.options["sdl"].sdl2main = self.settings.os != "iOS" - - 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"].openssl = not is_apple_os(self) - if self._isMobilePlatform: - self.options["qt"].opengl = "es2" - - # static Qt for iOS is the only viable option at the moment - if self.settings.os == "iOS": - self.options["qt"].shared = False - - # MSVC static build requires static runtime, but VCMI uses dynamic runtime - if is_msvc(self): - self.options["ffmpeg"].shared = True - def requirements(self): # client if self.options.with_ffmpeg: @@ -92,6 +76,26 @@ class VCMI(ConanFile): else: self.requires("qt/[~5.15.2]") + def validate(self): + # FFmpeg + if is_msvc(self) and self.options.with_ffmpeg and self.dependencies["ffmpeg"].options.shared != True: + raise ConanInvalidConfiguration("MSVC FFmpeg static build requires static runtime, but VCMI uses dynamic runtime. You must build FFmpeg as shared.") + + # SDL + sdl2mainValue = self.settings.os != "iOS" + if self.dependencies["sdl"].options.sdl2main != sdl2mainValue: + raise ConanInvalidConfiguration(f"sdl:sdl2main option for {self.settings.os} must be set to {sdl2mainValue}") + + # Qt + qtDep = self.dependencies["qt"] + if qtDep.options.qttools != True: + raise ConanInvalidConfiguration("qt:qttools option must be set to True") + if self.settings.os == "Android" and qtDep.options.qtandroidextras != True: + # TODO: in Qt 6 this option doesn't exist + raise ConanInvalidConfiguration("qt:qtandroidextras option for Android must be set to True") + if not is_apple_os(self) and qtDep.options.openssl != True: + raise ConanInvalidConfiguration("qt:openssl option for non-Apple OS must be set to True, otherwise mods can't be downloaded") + def _pathForCmake(self, path): # CMake doesn't like \ in strings return path.replace(os.path.sep, os.path.altsep) if os.path.altsep else path From 026e9c5f8fd5052491116c24c4903f7c923525a1 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sat, 15 Mar 2025 21:33:46 +0300 Subject: [PATCH 11/20] [conan][windows] allow Boost versions up to 1.87 when targeting Windows < 10 adds new option for that --- conanfile.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index b18d21062..3a047dc91 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,6 @@ class VCMI(ConanFile): generators = "CMakeDeps" _libRequires = [ - "boost/[^1.69]", "luajit/2.1.0-beta3", "minizip/[^1.2.12]", "zlib/[^1.2.12]", @@ -32,9 +31,11 @@ class VCMI(ConanFile): requires = _libRequires + _clientRequires + _launcherRequires options = { + "target_pre_windows10": [True, False], "with_ffmpeg": [True, False], } default_options = { + "target_pre_windows10": False, "with_ffmpeg": True, } @@ -53,7 +54,18 @@ class VCMI(ConanFile): if self.settings.os == "Android": self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") + if self.settings.os != "Windows": + del self.options.target_pre_windows10 + def requirements(self): + # lib + # boost::filesystem removed support for Windows < 10 in v1.87 + boostMinVersion = "1.69" + if self.options.get_safe("target_pre_windows10", False): + self.requires(f"boost/[>={boostMinVersion} <1.87]") + else: + self.requires(f"boost/[^{boostMinVersion}]") + # client if self.options.with_ffmpeg: self.requires("ffmpeg/[>=4.4]") From 45fffc5a50232dec0d634288fdbf10db293471fb Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Wed, 11 Jun 2025 22:18:33 +0300 Subject: [PATCH 12/20] move all Conan files to the dependencies repo --- CI/conan/android-32 | 12 -- CI/conan/android-32-ndk | 2 - CI/conan/android-64 | 5 - CI/conan/android-64-ndk | 2 - CI/conan/base/android | 30 ---- CI/conan/base/android-ndk | 2 - CI/conan/base/apple | 22 --- CI/conan/base/common | 223 ----------------------------- CI/conan/base/ios | 15 -- CI/conan/base/macos | 4 - CI/conan/base/meson_workaround.txt | 2 - CI/conan/base/msvc | 26 ---- CI/conan/base/vars.jinja | 1 - CI/conan/ios-arm64 | 5 - CI/conan/ios-armv7 | 8 -- CI/conan/macos-arm | 5 - CI/conan/macos-intel | 5 - CI/conan/msvc-x64 | 4 - CI/conan/msvc-x86 | 4 - conanfile.py | 152 -------------------- 20 files changed, 529 deletions(-) delete mode 100644 CI/conan/android-32 delete mode 100644 CI/conan/android-32-ndk delete mode 100644 CI/conan/android-64 delete mode 100644 CI/conan/android-64-ndk delete mode 100644 CI/conan/base/android delete mode 100644 CI/conan/base/android-ndk delete mode 100644 CI/conan/base/apple delete mode 100644 CI/conan/base/common delete mode 100644 CI/conan/base/ios delete mode 100644 CI/conan/base/macos delete mode 100644 CI/conan/base/meson_workaround.txt delete mode 100644 CI/conan/base/msvc delete mode 100644 CI/conan/base/vars.jinja delete mode 100644 CI/conan/ios-arm64 delete mode 100644 CI/conan/ios-armv7 delete mode 100644 CI/conan/macos-arm delete mode 100644 CI/conan/macos-intel delete mode 100644 CI/conan/msvc-x64 delete mode 100644 CI/conan/msvc-x86 delete mode 100644 conanfile.py diff --git a/CI/conan/android-32 b/CI/conan/android-32 deleted file mode 100644 index 61cd8dc7c..000000000 --- a/CI/conan/android-32 +++ /dev/null @@ -1,12 +0,0 @@ -include(base/android) - -[settings] -arch=armv7 -os.api_level=19 - -[conf] -# remove after switching to API level >= 24 -{% set file_funcs_macros = ['fseeko=fseek', 'ftello=ftell'] %} -flac/*:tools.build:defines={{ file_funcs_macros }} -luajit/*:tools.build:defines={{ file_funcs_macros }} -opusfile/*:tools.build:defines={{ file_funcs_macros }} diff --git a/CI/conan/android-32-ndk b/CI/conan/android-32-ndk deleted file mode 100644 index 05d3cd11c..000000000 --- a/CI/conan/android-32-ndk +++ /dev/null @@ -1,2 +0,0 @@ -include(android-32) -include(base/android-ndk) diff --git a/CI/conan/android-64 b/CI/conan/android-64 deleted file mode 100644 index d9ef364ae..000000000 --- a/CI/conan/android-64 +++ /dev/null @@ -1,5 +0,0 @@ -include(base/android) - -[settings] -arch=armv8 -os.api_level=21 diff --git a/CI/conan/android-64-ndk b/CI/conan/android-64-ndk deleted file mode 100644 index 25caf445e..000000000 --- a/CI/conan/android-64-ndk +++ /dev/null @@ -1,2 +0,0 @@ -include(android-64) -include(base/android-ndk) diff --git a/CI/conan/base/android b/CI/conan/base/android deleted file mode 100644 index a791b7df5..000000000 --- a/CI/conan/base/android +++ /dev/null @@ -1,30 +0,0 @@ -{% import 'vars.jinja' as vars %} - -include(common) - -[settings] -compiler=clang -compiler.cppstd={{ vars.cppstd }} -compiler.libcxx=c++_shared -compiler.version=14 -os=Android - -[replace_requires] -zlib/*: zlib/[*]@system - -[conf] -# https://github.com/conan-io/conan-center-index/issues/25342 -# https://github.com/conan-io/conan/issues/16468#issuecomment-2175877245 -tools.meson.mesontoolchain:extra_machine_files={{ [os.path.join(profile_dir, "meson_workaround.txt")] }} - -[options] -# TODO https://github.com/conan-io/conan-center-index/issues/26020 -# freetype/*:shared=True -sdl/*:shared=False -sdl_image/*:shared=False -sdl_mixer/*:shared=False -sdl_ttf/*:shared=False - -qt/*:opengl=es2 -qt/*:qtandroidextras=True -qt/*:with_freetype=True diff --git a/CI/conan/base/android-ndk b/CI/conan/base/android-ndk deleted file mode 100644 index 37e1baed9..000000000 --- a/CI/conan/base/android-ndk +++ /dev/null @@ -1,2 +0,0 @@ -[tool_requires] -android-ndk/r25c diff --git a/CI/conan/base/apple b/CI/conan/base/apple deleted file mode 100644 index ee16f30c0..000000000 --- a/CI/conan/base/apple +++ /dev/null @@ -1,22 +0,0 @@ -{% import 'vars.jinja' as vars %} - -include(common) - -[settings] -compiler=apple-clang -compiler.cppstd={{ vars.cppstd }} -compiler.libcxx=libc++ -compiler.version=16 - -[replace_requires] -bzip2/*: bzip2/[*]@system -libiconv/*: libiconv/[*]@system -sqlite3/*: sqlite3/[*]@system -zlib/*: zlib/[*]@system - -[conf] -tools.apple:enable_bitcode=False - -[options] -qt/*:openssl=False -sdl_image/*:imageio=True diff --git a/CI/conan/base/common b/CI/conan/base/common deleted file mode 100644 index cf151e0a9..000000000 --- a/CI/conan/base/common +++ /dev/null @@ -1,223 +0,0 @@ -[settings] -build_type=Release - -[platform_tool_requires] -cmake/3.31.6 -ninja/1.12.1 - -[replace_requires] -# TODO: remove after https://github.com/conan-io/conan-center-index/pull/27125 is merged -# Qt 5 depends on 0.4.8, but it doesn't build for iOS -md4c/0.4.8: md4c/0.5.2 - -[conf] -tools.cmake.cmaketoolchain:generator=Ninja - -[options] -# shared (dynamic) libs -boost/*:shared=True -bzip2/*:shared=True -libiconv/*:shared=True -libpng/*:shared=True -minizip/*:shared=True -ogg/*:shared=True -opus/*:shared=True -qt/*:shared=True -sdl/*:shared=True -sdl_image/*:shared=True -sdl_mixer/*:shared=True -sdl_ttf/*:shared=True -xz_utils/*:shared=True -zlib/*:shared=True - -# Boost -# we need only the following parts: -# date_time filesystem iostreams locale program_options system -# some other parts are also enabled because they're dependents -# see e.g. conan-center-index/recipes/boost/all/dependencies -boost/*:without_context=True -boost/*:without_contract=True -boost/*:without_coroutine=True -boost/*:without_fiber=True -boost/*:without_graph=True -boost/*:without_graph_parallel=True -boost/*:without_json=True -boost/*:without_log=True -boost/*:without_math=True -boost/*:without_mpi=True -boost/*:without_nowide=True -boost/*:without_process=True -boost/*:without_python=True -boost/*:without_serialization=True -boost/*:without_stacktrace=True -boost/*:without_test=True -boost/*:without_timer=True -boost/*:without_type_erasure=True -boost/*:without_wave=True -boost/*:without_url=True - -# FFmpeg -ffmpeg/*:avcodec=True -ffmpeg/*:avdevice=False -ffmpeg/*:avfilter=False -ffmpeg/*:avformat=True -ffmpeg/*:postproc=False -# for resampling of audio in 'planar' formats -ffmpeg/*:swresample=True -# for video scaling -ffmpeg/*:swscale=True - -ffmpeg/*:disable_all_bitstream_filters=True -ffmpeg/*:disable_all_decoders=True -ffmpeg/*:disable_all_demuxers=True -ffmpeg/*:disable_all_encoders=True -ffmpeg/*:disable_all_filters=True -ffmpeg/*:disable_all_hardware_accelerators=True -ffmpeg/*:disable_all_muxers=True -ffmpeg/*:disable_all_parsers=True -ffmpeg/*:disable_all_protocols=True - -ffmpeg/*:with_asm=False -ffmpeg/*:with_freetype=False -ffmpeg/*:with_libaom=False -ffmpeg/*:with_libdav1d=False -ffmpeg/*:with_libfdk_aac=False -ffmpeg/*:with_libiconv=False -ffmpeg/*:with_libmp3lame=False -ffmpeg/*:with_libsvtav1=False -ffmpeg/*:with_libvpx=False -ffmpeg/*:with_libwebp=False -ffmpeg/*:with_libx264=False -ffmpeg/*:with_libx265=False -ffmpeg/*:with_lzma=True -ffmpeg/*:with_openh264=False -ffmpeg/*:with_openjpeg=False -ffmpeg/*:with_programs=False -ffmpeg/*:with_sdl=False -ffmpeg/*:with_ssl=False -ffmpeg/*:with_vorbis=False - -# 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) -{% set ffDecoders = [ - 'bink', - 'binkaudio_dct', - 'binkaudio_rdft', - 'opus', - 'smackaud', - 'smacker', - 'theora', - 'vorbis', - 'vp8', - 'vp9', -] %} -{% set ffDemuxers = [ - 'bink', - 'binka', - 'ogg', - 'smacker', - 'webm_dash_manifest', -] %} -{% set ffParsers = [ - 'opus', - 'vorbis', - 'vp8', - 'vp9', - 'webp', -] %} -{% set ffProtocols = [ - 'file', -] %} -ffmpeg/*:enable_decoders={{ ffDecoders|join(',') }} -ffmpeg/*:enable_demuxers={{ ffDemuxers|join(',') }} -ffmpeg/*:enable_parsers={{ ffParsers|join(',') }} -ffmpeg/*:enable_protocols={{ ffProtocols|join(',') }} - -# optionally, for testing - enable ffplay/ffprobe binaries: -# ffmpeg/*:avfilter=True -# ffmpeg/*:enable_filters=aresample,scale -# ffmpeg/*:with_programs=True -# ffmpeg/*:with_sdl=True - -# oneTBB -onetbb/*:tbbbind=False -onetbb/*:tbbmalloc=False -onetbb/*:tbbproxy=False - -# Qt -# xpm format is required for Drag'n'Drop support -# we need only win/macdeployqt -# TODO: disabling these doesn't disable generation of CMake targets -# TODO: in Qt 6.3 it's a part of qtbase -# '-no-feature-assistant', -# '-no-feature-designer', -# '-no-feature-distancefieldgenerator', -# '-no-feature-kmap2qmap', -# '-no-feature-linguist', -# '-no-feature-makeqpf', -# '-no-feature-pixeltool', -# '-no-feature-qdbus', -# '-no-feature-qev', -# '-no-feature-qtattributionsscanner', -# '-no-feature-qtdiag', -# '-no-feature-qtpaths', -# '-no-feature-qtplugininfo', -{% set qtConfig = [ - '-no-gif', - '-no-ico', - - '-no-feature-imageformat_bmp', - '-no-feature-imageformat_jpeg', - '-no-feature-imageformat_ppm', - '-no-feature-imageformat_xbm', -] %} -qt/*:config={{ qtConfig|join(' ') }} -qt/*:essential_modules=False -qt/*:openssl=True -qt/*:qttools=True -qt/*:with_freetype=False -qt/*:with_libjpeg=False -qt/*:with_mysql=False -qt/*:with_odbc=False -qt/*:with_openal=False -qt/*:with_pq=False - -# SDL -sdl/*:iconv=True -sdl/*:sdl2main=True -sdl/*:vulkan=False - -# SDL_image -# 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 -sdl_image/*:gif=False -sdl_image/*:lbm=False -sdl_image/*:pcx=False -sdl_image/*:pnm=False -sdl_image/*:qoi=False -sdl_image/*:svg=False -sdl_image/*:tga=False -sdl_image/*:with_libjpeg=False -sdl_image/*:with_libtiff=False -sdl_image/*:with_libwebp=False -sdl_image/*:xcf=False -sdl_image/*:xpm=False -sdl_image/*:xv=False - -# SDL_mixer -# mp3, ogg and wav are the only ones that needs to be supported, flac is a bonus -sdl_mixer/*:mad=False -sdl_mixer/*:mikmod=False -sdl_mixer/*:modplug=False -sdl_mixer/*:nativemidi=False -sdl_mixer/*:tinymidi=False - -# transitive deps, stuff that's not needed -# doesn't link to bzip2 & zlib from "system" recipes -pcre2/*:build_pcre2grep=False -sqlite3/*:build_executable=False -opusfile/*:http=False -zstd/*:build_programs=False diff --git a/CI/conan/base/ios b/CI/conan/base/ios deleted file mode 100644 index 1f2230abf..000000000 --- a/CI/conan/base/ios +++ /dev/null @@ -1,15 +0,0 @@ -include(apple) - -[settings] -os=iOS -os.sdk=iphoneos - -[options] -qt/*:shared=False -sdl/*:shared=False -sdl_image/*:shared=False -sdl_mixer/*:shared=False -sdl_ttf/*:shared=False - -qt/*:opengl=es2 -sdl/*:sdl2main=False diff --git a/CI/conan/base/macos b/CI/conan/base/macos deleted file mode 100644 index db13d6a90..000000000 --- a/CI/conan/base/macos +++ /dev/null @@ -1,4 +0,0 @@ -include(apple) - -[settings] -os=Macos diff --git a/CI/conan/base/meson_workaround.txt b/CI/conan/base/meson_workaround.txt deleted file mode 100644 index 2e2bdae7b..000000000 --- a/CI/conan/base/meson_workaround.txt +++ /dev/null @@ -1,2 +0,0 @@ -[properties] -sys_root = '' diff --git a/CI/conan/base/msvc b/CI/conan/base/msvc deleted file mode 100644 index 6ef76c828..000000000 --- a/CI/conan/base/msvc +++ /dev/null @@ -1,26 +0,0 @@ -{% import 'vars.jinja' as vars %} - -include(common) - -[settings] -compiler=msvc -compiler.cppstd={{ vars.cppstd }} -compiler.runtime=dynamic -compiler.version=194 -os=Windows - -[conf] -# https://walbourn.github.io/a-brief-history-of-windows-sdks/ -# https://learn.microsoft.com/en-us/cpp/porting/modifying-winver-and-win32-winnt -# https://learn.microsoft.com/en-us/windows/win32/WinProg/using-the-windows-headers -{% set _WIN32_WINNT_WIN7 = '0x0601' %} -{% set NTDDI_WIN7 = '0x06010000' %} -{% set win7_defines = [ - '_WIN32_WINNT={}'.format(_WIN32_WINNT_WIN7), - 'WINVER={}'.format(_WIN32_WINNT_WIN7), - 'NTDDI_VERSION={}'.format(NTDDI_WIN7), -] %} -tools.build:defines={{ win7_defines }} - -[options] -ffmpeg/*:shared=True diff --git a/CI/conan/base/vars.jinja b/CI/conan/base/vars.jinja deleted file mode 100644 index cfa2d90c7..000000000 --- a/CI/conan/base/vars.jinja +++ /dev/null @@ -1 +0,0 @@ -{% set cppstd = '17' %} diff --git a/CI/conan/ios-arm64 b/CI/conan/ios-arm64 deleted file mode 100644 index 238bbcf8f..000000000 --- a/CI/conan/ios-arm64 +++ /dev/null @@ -1,5 +0,0 @@ -include(base/ios) - -[settings] -os.version=12.0 -arch=armv8 diff --git a/CI/conan/ios-armv7 b/CI/conan/ios-armv7 deleted file mode 100644 index 54482ce16..000000000 --- a/CI/conan/ios-armv7 +++ /dev/null @@ -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 diff --git a/CI/conan/macos-arm b/CI/conan/macos-arm deleted file mode 100644 index d3f06e078..000000000 --- a/CI/conan/macos-arm +++ /dev/null @@ -1,5 +0,0 @@ -include(base/macos) - -[settings] -os.version=11.0 -arch=armv8 diff --git a/CI/conan/macos-intel b/CI/conan/macos-intel deleted file mode 100644 index b527e056b..000000000 --- a/CI/conan/macos-intel +++ /dev/null @@ -1,5 +0,0 @@ -include(base/macos) - -[settings] -os.version=10.13 -arch=x86_64 diff --git a/CI/conan/msvc-x64 b/CI/conan/msvc-x64 deleted file mode 100644 index 9f2334126..000000000 --- a/CI/conan/msvc-x64 +++ /dev/null @@ -1,4 +0,0 @@ -include(base/msvc) - -[settings] -arch=x86_64 diff --git a/CI/conan/msvc-x86 b/CI/conan/msvc-x86 deleted file mode 100644 index b70e318d9..000000000 --- a/CI/conan/msvc-x86 +++ /dev/null @@ -1,4 +0,0 @@ -include(base/msvc) - -[settings] -arch=x86 diff --git a/conanfile.py b/conanfile.py deleted file mode 100644 index 3a047dc91..000000000 --- a/conanfile.py +++ /dev/null @@ -1,152 +0,0 @@ -from conan import ConanFile -from conan.errors import ConanInvalidConfiguration -from conan.tools.apple import is_apple_os -from conan.tools.cmake import CMakeToolchain -from conan.tools.files import save -from conan.tools.microsoft import is_msvc - -from glob import glob -import os - -required_conan_version = ">=2.13.0" - -class VCMI(ConanFile): - settings = "os", "compiler", "build_type", "arch" - generators = "CMakeDeps" - - _libRequires = [ - "luajit/2.1.0-beta3", - "minizip/[^1.2.12]", - "zlib/[^1.2.12]", - ] - _clientRequires = [ - "onetbb/[^2021.7]", - "sdl_image/[^2.8.2]", - "sdl_mixer/[^2.8.0]", - "sdl_ttf/[^2.0.18]", - ] - _launcherRequires = [ - "xz_utils/[^5.2.5]", # innoextract - ] - requires = _libRequires + _clientRequires + _launcherRequires - - options = { - "target_pre_windows10": [True, False], - "with_ffmpeg": [True, False], - } - default_options = { - "target_pre_windows10": False, - "with_ffmpeg": True, - } - - @property - def _isMobilePlatform(self): - return self.settings.os == "iOS" or self.settings.os == "Android" - - def config_options(self): - # static on "single app" platforms - isSdlShared = not self._isMobilePlatform - self.options["sdl"].shared = isSdlShared - self.options["sdl_image"].shared = isSdlShared - self.options["sdl_mixer"].shared = isSdlShared - self.options["sdl_ttf"].shared = isSdlShared - - if self.settings.os == "Android": - self.options["qt"].android_sdk = os.getenv("ANDROID_HOME") - - if self.settings.os != "Windows": - del self.options.target_pre_windows10 - - def requirements(self): - # lib - # boost::filesystem removed support for Windows < 10 in v1.87 - boostMinVersion = "1.69" - if self.options.get_safe("target_pre_windows10", False): - self.requires(f"boost/[>={boostMinVersion} <1.87]") - else: - self.requires(f"boost/[^{boostMinVersion}]") - - # client - if self.options.with_ffmpeg: - self.requires("ffmpeg/[>=4.4]") - - # On Android SDL version must be the same as the version of Java wrapper for SDL in VCMI source code - # Wrapper can be found in the following directory: android/vcmi-app/src/main/java/org/libsdl/app - # TODO: try enabling version range once there's no conflict - # sdl_image & sdl_ttf depend on earlier version - # ERROR: Version conflict: Conflict between sdl/2.28.5 and sdl/2.28.3 in the graph. - # Conflict originates from sdl_mixer/2.8.0 - # 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.1 || >=2.0.20 <=2.22.0]") - # versions before 2.30.7 don't build for Android with NDK 27: https://github.com/libsdl-org/SDL/issues/9792 - self.requires("sdl/2.30.9", override=True) - - # launcher - if self.settings.os == "Android": - self.requires("qt/[~5.15.14]") # earlier versions have serious bugs - else: - self.requires("qt/[~5.15.2]") - - def validate(self): - # FFmpeg - if is_msvc(self) and self.options.with_ffmpeg and self.dependencies["ffmpeg"].options.shared != True: - raise ConanInvalidConfiguration("MSVC FFmpeg static build requires static runtime, but VCMI uses dynamic runtime. You must build FFmpeg as shared.") - - # SDL - sdl2mainValue = self.settings.os != "iOS" - if self.dependencies["sdl"].options.sdl2main != sdl2mainValue: - raise ConanInvalidConfiguration(f"sdl:sdl2main option for {self.settings.os} must be set to {sdl2mainValue}") - - # Qt - qtDep = self.dependencies["qt"] - if qtDep.options.qttools != True: - raise ConanInvalidConfiguration("qt:qttools option must be set to True") - if self.settings.os == "Android" and qtDep.options.qtandroidextras != True: - # TODO: in Qt 6 this option doesn't exist - raise ConanInvalidConfiguration("qt:qtandroidextras option for Android must be set to True") - if not is_apple_os(self) and qtDep.options.openssl != True: - raise ConanInvalidConfiguration("qt:openssl option for non-Apple OS must be set to True, otherwise mods can't be downloaded") - - def _pathForCmake(self, path): - # CMake doesn't like \ in strings - return path.replace(os.path.sep, os.path.altsep) if os.path.altsep else path - - def _generateRuntimeLibsFile(self): - # 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 += 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_RUNTIME_LIBS_FILE"] = self._generateRuntimeLibsFile() - if self.settings.os == "Android": - tc.variables["CMAKE_ANDROID_API"] = str(self.settings.os.api_level) - elif self.settings.os == "Windows": - tc.variables["CONAN_RUNENV_SCRIPT"] = self._pathForCmake(os.path.join(self.build_folder, "conanrun.bat")) - tc.generate() From adc56584f7c69d88038b68a7501a59dcf5a482f5 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Fri, 8 Aug 2025 22:02:20 +0300 Subject: [PATCH 13/20] add vcmi-dependencies as submodule --- .gitmodules | 3 +++ conanfile.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ dependencies | 1 + 3 files changed, 58 insertions(+) create mode 100644 conanfile.py create mode 160000 dependencies diff --git a/.gitmodules b/.gitmodules index d6b206fe1..57ce2608d 100644 --- a/.gitmodules +++ b/.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 diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 000000000..b4db173f3 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,54 @@ +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_RUNTIME_LIBS_FILE"] = self._generateRuntimeLibsFile() + if self.settings.os == "Android": + tc.variables["CMAKE_ANDROID_API"] = str(self.settings.os.api_level) + tc.variables["SDL_JAVA_SRC_DIR"] = os.path.join(self.dependencies.host["sdl"].package_folder, "share", "java", "SDL2") + elif self.settings.os == "Windows": + tc.variables["CONAN_RUNENV_SCRIPT"] = self._pathForCmake(os.path.join(self.build_folder, "conanrun.bat")) + tc.generate() diff --git a/dependencies b/dependencies new file mode 160000 index 000000000..b6f03bd54 --- /dev/null +++ b/dependencies @@ -0,0 +1 @@ +Subproject commit b6f03bd541f19ad441ffb930a2cbd000222a50bf From 996bde3c6090dfd186edffc1fcb1fbb680388918 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Sat, 9 Aug 2025 23:37:49 +0300 Subject: [PATCH 14/20] [android] use SDL Java sources from the Conan package --- android/vcmi-app/build.gradle | 2 +- .../main/java/org/libsdl/app/HIDDevice.java | 22 - .../app/HIDDeviceBLESteamController.java | 650 ----- .../java/org/libsdl/app/HIDDeviceManager.java | 679 ------ .../java/org/libsdl/app/HIDDeviceUSB.java | 309 --- .../src/main/java/org/libsdl/app/SDL.java | 85 - .../main/java/org/libsdl/app/SDLActivity.java | 2102 ----------------- .../java/org/libsdl/app/SDLAudioManager.java | 394 --- .../org/libsdl/app/SDLControllerManager.java | 814 ------- .../main/java/org/libsdl/app/SDLSurface.java | 405 ---- clientapp/CMakeLists.txt | 3 +- 11 files changed, 3 insertions(+), 5462 deletions(-) delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/SDL.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/SDLAudioManager.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/SDLControllerManager.java delete mode 100644 android/vcmi-app/src/main/java/org/libsdl/app/SDLSurface.java diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 49d2bf511..31de2b2a5 100644 --- a/android/vcmi-app/build.gradle +++ b/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'] } diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java b/android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java deleted file mode 100644 index 955df5d14..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDevice.java +++ /dev/null @@ -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(); -} diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java deleted file mode 100644 index 65c5a4237..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ /dev/null @@ -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 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(); - 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(); - } - -} - diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java deleted file mode 100644 index cf3c9267f..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ /dev/null @@ -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 mDevicesById = new HashMap(); - private HashMap mBluetoothDevices = new HashMap(); - private int mNextDeviceId = 0; - private SharedPreferences mSharedPreferences = null; - private boolean mIsChromebook = false; - private UsbManager mUsbManager; - private Handler mHandler; - private BluetoothManager mBluetoothManager; - private List 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 devices = new ArrayList(); - 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(); - - // 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 disconnected = new ArrayList(); - ArrayList connected = new ArrayList(); - - List 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); -} diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java deleted file mode 100644 index d20fe80bc..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ /dev/null @@ -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); - } - } - } - } - } -} diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/SDL.java b/android/vcmi-app/src/main/java/org/libsdl/app/SDL.java deleted file mode 100644 index dafc0cb87..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/SDL.java +++ /dev/null @@ -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; -} diff --git a/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java b/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java deleted file mode 100644 index 649fb1e74..000000000 --- a/android/vcmi-app/src/main/java/org/libsdl/app/SDLActivity.java +++ /dev/null @@ -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 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(); - 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