瀏覽代碼

[launcher] build for Android

also embeds icons and translations as Qt resources instead of reading from disk
Andrey Filipenkov 1 年之前
父節點
當前提交
8cee8b72a6

+ 26 - 32
CMakeLists.txt

@@ -58,13 +58,22 @@ option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" O
 # Platform-specific options
 
 if(ANDROID)
+	set(ANDROID_TARGET_SDK_VERSION "33" CACHE STRING "Android target SDK version")
+	set(ANDROIDDEPLOYQT_OPTIONS "" CACHE STRING "Additional androiddeployqt options separated by semi-colon")
+	set(ANDROID_GRADLE_PROPERTIES "" CACHE STRING "Additional Gradle properties separated by semi-colon")
+
 	set(ENABLE_STATIC_LIBS ON)
-	set(ENABLE_LAUNCHER OFF)
+	set(ENABLE_LAUNCHER ON)
 else()
 	option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF)
 	option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
 endif()
 
+if(APPLE_IOS)
+	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
+	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
+endif()
+
 if(APPLE_IOS OR ANDROID)
 	set(ENABLE_MONOLITHIC_INSTALL OFF)
 	set(ENABLE_SINGLE_APP_BUILD ON)
@@ -100,11 +109,6 @@ if (ENABLE_STATIC_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
 	set(CMAKE_POSITION_INDEPENDENT_CODE ON)
 endif()
 
-if(APPLE_IOS)
-	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
-	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
-endif()
-
 if(ENABLE_COLORIZED_COMPILER_OUTPUT)
 	if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
 		add_compile_options(-fcolor-diagnostics)
@@ -147,10 +151,6 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR
 
 include(VCMIUtils)
 include(VersionDefinition)
-if(ANDROID)
-	set(VCMI_VERSION "${APP_SHORT_VERSION}")
-	configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY)
-endif()
 
 vcmi_print_important_variables()
 
@@ -575,8 +575,12 @@ elseif(APPLE)
 	endif()
 elseif(ANDROID)
 	include(GNUInstallDirs)
-	set(LIB_DIR "jniLibs/${ANDROID_ABI}")
-	set(DATA_DIR "assets")
+	set(LIB_DIR "libs/${ANDROID_ABI}")
+
+	# required by Qt
+	set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android")
+	set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build")
+	set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${androidQtBuildDir}/${LIB_DIR}")
 else()
 	# includes lib path which determines where to install shared libraries (either /lib or /lib64)
 	include(GNUInstallDirs)
@@ -621,6 +625,13 @@ else()
 	set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting")
 endif()
 
+# common Qt paths
+if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
+	get_target_property(qmakePath Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION)
+	get_filename_component(qtDir "${qmakePath}/../../" ABSOLUTE)
+	set(qtBinDir "${qtDir}/bin")
+endif()
+
 #######################################
 #        Add subdirectories           #
 #######################################
@@ -682,32 +693,15 @@ endif()
 #######################################
 
 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}")
+
 	if(ANDROID_STL MATCHES "_shared$")
 		set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}")
 		install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}"
 			DESTINATION ${LIB_DIR}
 		)
 	endif()
-
-	# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip
-	install(CODE "
-		cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX
-			OUTPUT_VARIABLE absolute_install_prefix
-		)
-		set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\")
-		file(MAKE_DIRECTORY \"\${absolute_data_dir}\")
-
-		set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\")
-		execute_process(COMMAND
-			\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods
-			WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"
-		)
-
-		file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5)
-		file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\"
-			\${internal_data_zip_md5}
-		)
-	")
 else()
 	install(DIRECTORY config DESTINATION ${DATA_DIR})
 	if (ENABLE_CLIENT OR ENABLE_SERVER)

+ 14 - 3
client/CMakeLists.txt

@@ -395,6 +395,9 @@ assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc)
 
 if(ANDROID)
 	add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS})
+	set_target_properties(vcmiclient PROPERTIES
+		OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
+	)
 else()
 	add_executable(vcmiclient ${client_SRCS} ${client_HEADERS})
 endif()
@@ -517,11 +520,19 @@ if(APPLE_IOS)
 	)
 	install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
 elseif(ANDROID)
+	find_program(androidDeployQt androiddeployqt
+		PATHS "${qtBinDir}"
+	)
 	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
-	add_custom_command(TARGET vcmiclient POST_BUILD
-		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main"
+
+	add_custom_target(android_deploy ALL
+		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
+		COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
+		COMMAND_EXPAND_LISTS
+		VERBATIM
+		COMMENT "Create android package"
 	)
-	install(TARGETS vcmiclient DESTINATION ${LIB_DIR})
+	add_dependencies(android_deploy vcmiclient)
 else()
 	install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
 endif()

+ 11 - 5
cmake_modules/VCMIUtils.cmake

@@ -7,16 +7,22 @@
 macro(vcmi_set_output_dir name dir)
 	# Multi-config builds for Visual Studio, Xcode
 	foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
-		 string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIGUPPERCASE)
-		 set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
-		 set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
-		 set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
+		string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIGUPPERCASE)
+		set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
+		if(ANDROID)
+			set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
+		else()
+			set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
+		endif()
+		set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
 	endforeach()
 
 	# Generic no-config case for Makefiles, Ninja.
 	# This is what Qt Creator is using
 	set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
-	set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
+	if(NOT ANDROID)
+		set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
+	endif()
 	set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
 endmacro()
 

+ 105 - 59
launcher/CMakeLists.txt

@@ -21,7 +21,13 @@ set(launcher_SRCS
 		launcherdirs.cpp
 		jsonutils.cpp
 		updatedialog_moc.cpp
+		prepare.cpp
 )
+if(APPLE_IOS)
+	list(APPEND launcher_SRCS
+		ios/launchGame.m
+	)
+endif()
 
 set(launcher_HEADERS
 		StdInc.h
@@ -40,7 +46,8 @@ set(launcher_HEADERS
 		jsonutils.h
 		updatedialog_moc.h
 		main.h
-		helper.cpp
+		helper.h
+		prepare.h
 )
 
 set(launcher_FORMS
@@ -53,30 +60,60 @@ set(launcher_FORMS
 		updatedialog_moc.ui
 )
 
+set(launcher_RESOURCES
+	resources.qrc
+)
+
+set(translationsDir "translation")
 set(launcher_TS
-	translation/chinese.ts
-	translation/czech.ts
-	translation/english.ts
-	translation/french.ts
-	translation/german.ts
-	translation/polish.ts
-	translation/portuguese.ts	
-	translation/russian.ts
-	translation/spanish.ts
-	translation/ukrainian.ts
-	translation/vietnamese.ts
+	"${translationsDir}/chinese.ts"
+	"${translationsDir}/czech.ts"
+	"${translationsDir}/english.ts"
+	"${translationsDir}/french.ts"
+	"${translationsDir}/german.ts"
+	"${translationsDir}/polish.ts"
+	"${translationsDir}/portuguese.ts"
+	"${translationsDir}/russian.ts"
+	"${translationsDir}/spanish.ts"
+	"${translationsDir}/ukrainian.ts"
+	"${translationsDir}/vietnamese.ts"
 )
+if(ENABLE_TRANSLATIONS)
+	if(TARGET Qt5::Core)
+		file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${translationsDir}")
+		set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION "${translationsDir}")
+		qt5_add_translation(launcher_QM ${launcher_TS})
+
+		set(translationsResource "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc")
+		list(APPEND launcher_RESOURCES "${translationsResource}")
+
+		set(rccQmFiles "")
+		foreach(qmFile ${launcher_QM})
+			string(APPEND rccQmFiles "<file>${qmFile}</file>\n")
+		endforeach()
+		file(WRITE "${translationsResource}"
+"<!DOCTYPE RCC>
+<RCC version=\"1.0\">
+<qresource prefix=\"/\">
+${rccQmFiles}
+</qresource>
+</RCC>"
+		)
+	endif()
+endif()
 
-if(APPLE_IOS)
-	list(APPEND launcher_SRCS
-		ios/main.m
-	)
+if(WIN32)
+	set(launcher_ICON VCMI_launcher.rc)
 endif()
 
-assign_source_group(${launcher_SRCS} ${launcher_HEADERS} VCMI_launcher.rc)
+assign_source_group(${launcher_SRCS} ${launcher_HEADERS} ${launcher_RESOURCES} ${launcher_TS} ${launcher_ICON})
 
-# Tell CMake to run moc when necessary:
+# TODO: enabling AUTORCC breaks msvc build on CI
 set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+if(NOT (MSVC AND "$ENV{GITHUB_ACTIONS}" STREQUAL true))
+	set(CMAKE_AUTORCC ON)
+endif()
 
 if(POLICY CMP0071)
 	cmake_policy(SET CMP0071 NEW)
@@ -86,38 +123,55 @@ endif()
 # to always look for includes there:
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 
-if(TARGET Qt6::Core)
-	qt_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
+if(ENABLE_SINGLE_APP_BUILD OR ANDROID)
+	add_library(vcmilauncher OBJECT ${launcher_QM})
 else()
-	qt5_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
-	if(ENABLE_TRANSLATIONS)
-		set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
-		qt5_add_translation( launcher_QM ${launcher_TS} )
-	endif()
+	add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_ICON})
 endif()
 
-
-if(WIN32)
-	set(launcher_ICON VCMI_launcher.rc)
-endif()
-
-if(ENABLE_SINGLE_APP_BUILD)
-	add_library(vcmilauncher STATIC ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS})
-else()
-	add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS} ${launcher_ICON})
-endif()
-
-if(TARGET Qt6::Core)
-	if(ENABLE_TRANSLATIONS)
-		set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
+if(ENABLE_TRANSLATIONS)
+	if(TARGET Qt6::Core)
 		qt_add_translations(vcmilauncher
 			TS_FILES ${launcher_TS}
-			QM_FILES_OUTPUT_VARIABLE launcher_QM
+			RESOURCE_PREFIX "/${translationsDir}"
 			INCLUDE_DIRECTORIES
 				${CMAKE_CURRENT_BINARY_DIR})
 	endif()
 endif()
 
+if(ANDROID)
+	get_target_property(rccPath Qt${QT_VERSION_MAJOR}::rcc IMPORTED_LOCATION)
+	get_filename_component(qtDir "${rccPath}/../../" ABSOLUTE)
+	set(qtDir "${qtDir}" PARENT_SCOPE)
+ 
+	function(generate_binary_resource resourceName resourceDir)
+		file(CREATE_LINK "${resourceDir}" "${CMAKE_CURRENT_BINARY_DIR}/${resourceName}"
+			COPY_ON_ERROR
+			SYMBOLIC
+		)
+		set(qrcFile "${CMAKE_CURRENT_BINARY_DIR}/${resourceName}.qrc")
+		execute_process(COMMAND
+			"${rccPath}" --project
+			WORKING_DIRECTORY "${resourceDir}"
+			OUTPUT_VARIABLE rccOutput
+		)
+		# add parent directory
+		string(REPLACE "<file>." "<file>${resourceName}" rccOutput "${rccOutput}")
+		file(WRITE "${qrcFile}" "${rccOutput}")
+	endfunction()
+ 
+	generate_binary_resource("config" "${CMAKE_SOURCE_DIR}/config")
+	list(APPEND launcher_RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/config.qrc")
+	generate_binary_resource("Mods" "${CMAKE_SOURCE_DIR}/Mods")
+	list(APPEND launcher_RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/Mods.qrc")
+endif()
+
+target_sources(vcmilauncher PRIVATE
+	${launcher_SRCS}
+	${launcher_HEADERS}
+	${launcher_RESOURCES}
+)
+
 if(WIN32)
 	set_target_properties(vcmilauncher
 		PROPERTIES
@@ -139,7 +193,9 @@ if(APPLE)
 	set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmilauncher)
 endif()
 
-if (NOT APPLE_IOS AND NOT ANDROID)
+if(ANDROID)
+	target_link_libraries(vcmilauncher Qt${QT_VERSION_MAJOR}::AndroidExtras)
+elseif(NOT APPLE_IOS)
 	target_link_libraries(vcmilauncher SDL2::SDL2)
 endif()
 
@@ -155,9 +211,7 @@ if(ENABLE_INNOEXTRACT)
 endif()
 
 if(APPLE_IOS)
-	set(RESOURCES_DESTINATION ${DATA_DIR})
-
-	# TODO: remove after fixing Conan's Qt recipe
+	# 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()
@@ -167,22 +221,19 @@ if(APPLE_IOS)
 		file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h
 			"#include <QtPlugin>\nQ_IMPORT_PLUGIN(QIOSIntegrationPlugin)"
 		)
-		# target_include_directories(vcmilauncher PRIVATE ${CMAKE_BINARY_DIR})
 		target_link_libraries(vcmilauncher
 			Qt${QT_VERSION_MAJOR}::QIOSIntegrationPlugin
 			qt::QIOSIntegrationPlugin
 		)
 	endif()
-else()
-	set(RESOURCES_DESTINATION ${DATA_DIR}/launcher)
-
-	# Link to build directory for easier debugging
-	add_custom_command(TARGET vcmilauncher POST_BUILD
-		COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher
-		COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
-		COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation
+elseif(ANDROID)
+	set(androidSdkDir "$ENV{ANDROID_HOME}")
+	configure_file(
+		"${androidPackageSourceDir}/androiddeployqt.json.in"
+		"${CMAKE_BINARY_DIR}/androiddeployqt.json"
+		@ONLY
 	)
-
+else()
 	install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})
 
 	# Install icons and desktop file on Linux
@@ -191,8 +242,3 @@ else()
 		install(FILES "eu.vcmi.VCMI.metainfo.xml" DESTINATION share/metainfo)
 	endif()
 endif()
-
-install(DIRECTORY icons DESTINATION ${RESOURCES_DESTINATION})
-if(ENABLE_TRANSLATIONS)
-	install(FILES ${launcher_QM} DESTINATION ${RESOURCES_DESTINATION}/translation)
-endif()

+ 4 - 3
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -363,7 +363,7 @@ void FirstLaunchView::extractGogData()
 
 void FirstLaunchView::copyHeroesData(const QString & path, bool move)
 {
-	QDir sourceRoot = QDir(path);
+	QDir sourceRoot{path};
 	
 	if(path.isEmpty())
 		sourceRoot.setPath(QFileDialog::getExistingDirectory(this, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks));
@@ -387,9 +387,10 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
 	QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
 	QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);
 
+	const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.");
 	if(dirData.empty())
 	{
-		QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data."));
+		QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
 		return;
 	}
 
@@ -403,7 +404,7 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
 		if (roeFiles.empty())
 		{
 			// Directory structure is correct (Data/Maps/Mp3) but no .lod archives that should be present in any install
-			QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data."));
+			QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
 			return;
 		}
 

+ 0 - 1
launcher/firstLaunch/firstlaunch_moc.h

@@ -96,5 +96,4 @@ private slots:
 
 private:
 	Ui::FirstLaunchView * ui;
-
 };

+ 0 - 0
launcher/ios/main.m → launcher/ios/launchGame.m


+ 30 - 19
launcher/main.cpp

@@ -10,24 +10,35 @@
 #include "StdInc.h"
 #include "main.h"
 #include "mainwindow_moc.h"
-#include "launcherdirs.h"
+#include "prepare.h"
 
 #include "../lib/VCMIDirs.h"
 
 #include <QApplication>
-#include <QProcess>
-#include <QMessageBox>
 
 // Conan workaround https://github.com/conan-io/conan-center-index/issues/13332
 #ifdef VCMI_IOS
-#if __has_include("QIOSIntegrationPlugin.h")
-#include "QIOSIntegrationPlugin.h"
-#endif
+# if __has_include("QIOSIntegrationPlugin.h")
+#  include "QIOSIntegrationPlugin.h"
+# endif
 int argcForClient;
 char ** argvForClient;
-#endif
+#elif defined(VCMI_ANDROID)
+# include <QAndroidJniObject>
+# include <QtAndroid>
+#else
+# include <QMessageBox>
+# include <QProcess>
+#endif // VCMI_IOS
 
-int main(int argc, char * argv[])
+// android must export main explicitly to make it visible in the shared library
+#ifdef VCMI_ANDROID
+# define MAIN_EXPORT ELF_VISIBILITY
+#else
+# define MAIN_EXPORT
+#endif // VCMI_ANDROID
+
+int MAIN_EXPORT main(int argc, char * argv[])
 {
 	int result;
 #ifdef VCMI_IOS
@@ -35,7 +46,7 @@ int main(int argc, char * argv[])
 #endif
 	QApplication vcmilauncher(argc, argv);
 
-	CLauncherDirs::prepare();
+	launcher::prepare();
 
 	MainWindow mainWindow;
 	mainWindow.show();
@@ -53,7 +64,7 @@ void startGame(const QStringList & args)
 {
 	logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString());
 
-#ifdef Q_OS_IOS
+#ifdef VCMI_IOS
 	static const char clientName[] = "vcmiclient";
 	argcForClient = args.size() + 1; //first argument is omitted
 	argvForClient = new char*[argcForClient];
@@ -61,11 +72,13 @@ void startGame(const QStringList & args)
 	strcpy(argvForClient[0], clientName);
 	for(int i = 1; i < argcForClient; ++i)
 	{
-        std::string s = args.at(i - 1).toStdString();
-        argvForClient[i] = new char[s.size() + 1];
-        strcpy(argvForClient[i], s.c_str());
+		std::string s = args.at(i - 1).toStdString();
+		argvForClient[i] = new char[s.size() + 1];
+		strcpy(argvForClient[i], s.c_str());
 	}
 	qApp->quit();
+#elif defined(VCMI_ANDROID)
+	QtAndroid::androidActivity().callMethod<void>("onLaunchGameBtnPressed");
 #else
 	startExecutable(pathToQString(VCMIDirs::get().clientPath()), args);
 #endif
@@ -78,7 +91,7 @@ void startEditor(const QStringList & args)
 #endif
 }
 
-#ifndef Q_OS_IOS
+#ifndef VCMI_MOBILE
 void startExecutable(QString name, const QStringList & args)
 {
 	QProcess process;
@@ -91,11 +104,9 @@ void startExecutable(QString name, const QStringList & args)
 	else
 	{
 		QMessageBox::critical(qApp->activeWindow(),
-							  "Error starting executable",
-							  "Failed to start " + name + "\n"
-							  "Reason: " + process.errorString(),
-							  QMessageBox::Ok,
-							  QMessageBox::Ok);
+			QObject::tr("Error starting executable"),
+			QObject::tr("Failed to start %1\nReason: %2").arg(name, process.errorString())
+		);
 	}
 }
 #endif

+ 19 - 32
launcher/mainwindow_moc.cpp

@@ -29,7 +29,7 @@ void MainWindow::load()
 	// This is important on Mac for relative paths to work inside DMG.
 	QDir::setCurrent(QApplication::applicationDirPath());
 
-#ifndef VCMI_IOS
+#ifndef VCMI_MOBILE
 	console = new CConsoleHandler();
 #endif
 	CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Launcher_log.txt", console);
@@ -38,14 +38,6 @@ void MainWindow::load()
 	CResourceHandler::initialize();
 	CResourceHandler::load("config/filesystem.json");
 
-#ifdef Q_OS_IOS
-	QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().binaryPath() / "icons"));
-#else
-	for(auto & string : VCMIDirs::get().dataPaths())
-		QDir::addSearchPath("icons", pathToQString(string / "launcher" / "icons"));
-	QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "icons"));
-#endif
-
 	Helper::loadSettings();
 }
 
@@ -85,7 +77,15 @@ MainWindow::MainWindow(QWidget * parent)
 	updateTranslation(); // load translation
 
 	ui->setupUi(this);
-	
+
+	setWindowIcon(QIcon{":/icons/menu-game.png"});
+	ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"});
+	ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"});
+	ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"});
+	ui->startEditorButton->setIcon(QIcon{":/icons/menu-editor.png"});
+	ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"});
+
+#ifndef VCMI_MOBILE
 	//load window settings
 	QSettings s(Ui::teamName, Ui::appName);
 
@@ -99,6 +99,7 @@ MainWindow::MainWindow(QWidget * parent)
 	{
 		move(position);
 	}
+#endif
 
 #ifndef ENABLE_EDITOR
 	ui->startEditorButton->hide();
@@ -183,10 +184,12 @@ void MainWindow::changeEvent(QEvent *event)
 
 MainWindow::~MainWindow()
 {
+#ifndef VCMI_MOBILE
 	//save window settings
 	QSettings s(Ui::teamName, Ui::appName);
 	s.setValue("MainWindow/Size", size());
 	s.setValue("MainWindow/Position", pos());
+#endif
 
 	delete ui;
 }
@@ -231,32 +234,16 @@ void MainWindow::on_aboutButton_clicked()
 void MainWindow::updateTranslation()
 {
 #ifdef ENABLE_QT_TRANSLATIONS
-	std::string translationFile = settings["general"]["language"].String() + ".qm";
+	const std::string translationFile = settings["general"]["language"].String() + ".qm";
 	logGlobal->info("Loading translation '%s'", translationFile);
 
-	QVector<QString> searchPaths;
-
-#ifdef Q_OS_IOS
-	searchPaths.push_back(pathToQString(VCMIDirs::get().binaryPath() / "translation" / translationFile));
-#else
-	for(auto const & string : VCMIDirs::get().dataPaths())
-		searchPaths.push_back(pathToQString(string / "launcher" / "translation" / translationFile));
-	searchPaths.push_back(pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "translation" / translationFile));
-#endif
-
-	for(auto const & string : boost::adaptors::reverse(searchPaths))
+	if (!translator.load(QString{":/translation/%1"}.arg(translationFile.c_str())))
 	{
-		logGlobal->info("Searching for translation at '%s'", string.toStdString());
-		if (translator.load(string))
-		{
-			logGlobal->info("Translation found");
-			if (!qApp->installTranslator(&translator))
-				logGlobal->error("Failed to install translator");
-			return;
-		}
+		logGlobal->error("Failed to load translation");
+		return;
 	}
 
-	logGlobal->error("Failed to find translation");
-
+	if (!qApp->installTranslator(&translator))
+		logGlobal->error("Failed to install translator");
 #endif
 }

+ 0 - 24
launcher/mainwindow_moc.ui

@@ -19,10 +19,6 @@
   <property name="windowTitle">
    <string>VCMI Launcher</string>
   </property>
-  <property name="windowIcon">
-   <iconset>
-    <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
-  </property>
   <property name="iconSize">
    <size>
     <width>64</width>
@@ -56,10 +52,6 @@
         <property name="text">
          <string>Mods</string>
         </property>
-        <property name="icon">
-         <iconset>
-          <normaloff>icons:menu-mods.png</normaloff>icons:menu-mods.png</iconset>
-        </property>
         <property name="iconSize">
          <size>
           <width>64</width>
@@ -106,10 +98,6 @@
         <property name="text">
          <string>Settings</string>
         </property>
-        <property name="icon">
-         <iconset>
-          <normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
-        </property>
         <property name="iconSize">
          <size>
           <width>64</width>
@@ -156,10 +144,6 @@
         <property name="text">
          <string>Help</string>
         </property>
-        <property name="icon">
-         <iconset>
-          <normaloff>icons:about-project.png</normaloff>icons:about-project.png</iconset>
-        </property>
         <property name="iconSize">
          <size>
           <width>32</width>
@@ -225,10 +209,6 @@
         <property name="text">
          <string>Map Editor</string>
         </property>
-        <property name="icon">
-         <iconset>
-          <normaloff>icons:menu-editor.png</normaloff>icons:menu-editor.png</iconset>
-        </property>
         <property name="iconSize">
          <size>
           <width>32</width>
@@ -278,10 +258,6 @@
         <property name="text">
          <string>Start game</string>
         </property>
-        <property name="icon">
-         <iconset>
-          <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
-        </property>
         <property name="iconSize">
          <size>
           <width>64</width>

+ 5 - 5
launcher/modManager/cmodlistmodel_moc.cpp

@@ -14,11 +14,11 @@
 
 namespace ModStatus
 {
-static const QString iconDelete = "icons:mod-delete.png";
-static const QString iconDisabled = "icons:mod-disabled.png";
-static const QString iconDownload = "icons:mod-download.png";
-static const QString iconEnabled = "icons:mod-enabled.png";
-static const QString iconUpdate = "icons:mod-update.png";
+static const QString iconDelete = ":/icons/mod-delete.png";
+static const QString iconDisabled = ":/icons/mod-disabled.png";
+static const QString iconDownload = ":/icons/mod-download.png";
+static const QString iconEnabled = ":/icons/mod-enabled.png";
+static const QString iconUpdate = ":/icons/mod-update.png";
 }
 
 CModListModel::CModListModel(QObject * parent)

+ 11 - 4
launcher/modManager/cmodlistview_moc.cpp

@@ -130,6 +130,12 @@ CModListView::CModListView(QWidget * parent)
 
 	setAcceptDrops(true);
 
+	ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
+	ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
+	ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
+	ui->updateButton->setIcon(QIcon{":/icons/mod-update.png"});
+	ui->installButton->setIcon(QIcon{":/icons/mod-download.png"});
+
 	setupModModel();
 	setupFilterModel();
 	setupModsView();
@@ -393,14 +399,15 @@ void CModListView::selectMod(const QModelIndex & index)
 	}
 	else
 	{
-		auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString());
+		const auto modName = index.data(ModRoles::ModNameRole).toString();
+		auto mod = modModel->getMod(modName);
 
 		ui->modInfoBrowser->setHtml(genModInfoText(mod));
 		ui->changelogBrowser->setHtml(genChangelogText(mod));
 
-		bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty();
-		bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty();
-		bool hasDependentMods = !findDependentMods(index.data(ModRoles::ModNameRole).toString(), true).empty();
+		bool hasInvalidDeps = !findInvalidDependencies(modName).empty();
+		bool hasBlockingMods = !findBlockingMods(modName).empty();
+		bool hasDependentMods = !findDependentMods(modName, true).empty();
 
 		ui->disableButton->setVisible(mod.isEnabled());
 		ui->enableButton->setVisible(mod.isDisabled());

+ 0 - 20
launcher/modManager/cmodlistview_moc.ui

@@ -423,10 +423,6 @@ hr { height: 1px; border-width: 0; }
        <property name="text">
         <string>Uninstall</string>
        </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-delete.png</normaloff>icons:mod-delete.png</iconset>
-       </property>
        <property name="iconSize">
         <size>
          <width>20</width>
@@ -458,10 +454,6 @@ hr { height: 1px; border-width: 0; }
        <property name="text">
         <string>Enable</string>
        </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-enabled.png</normaloff>icons:mod-enabled.png</iconset>
-       </property>
        <property name="iconSize">
         <size>
          <width>20</width>
@@ -493,10 +485,6 @@ hr { height: 1px; border-width: 0; }
        <property name="text">
         <string>Disable</string>
        </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-disabled.png</normaloff>icons:mod-disabled.png</iconset>
-       </property>
        <property name="iconSize">
         <size>
          <width>20</width>
@@ -528,10 +516,6 @@ hr { height: 1px; border-width: 0; }
        <property name="text">
         <string>Update</string>
        </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-update.png</normaloff>icons:mod-update.png</iconset>
-       </property>
        <property name="iconSize">
         <size>
          <width>20</width>
@@ -563,10 +547,6 @@ hr { height: 1px; border-width: 0; }
        <property name="text">
         <string>Install</string>
        </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-download.png</normaloff>icons:mod-download.png</iconset>
-       </property>
        <property name="iconSize">
         <size>
          <width>20</width>

+ 3 - 1
launcher/modManager/cmodmanager.cpp

@@ -20,6 +20,8 @@
 #include "../jsonutils.h"
 #include "../launcherdirs.h"
 
+#include <future>
+
 namespace
 {
 QString detectModArchive(QString path, QString modName, std::vector<std::string> & filesToExtract)
@@ -360,7 +362,7 @@ bool CModManager::removeModDir(QString path)
 	
 	if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive))
 		return false;
-#ifndef VCMI_IOS //ios applications are stored in the isolated container
+#ifndef VCMI_MOBILE // ios and android applications are stored in the isolated container
 	if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive))
 		return false;
 

+ 85 - 0
launcher/prepare.cpp

@@ -0,0 +1,85 @@
+/*
+ * prepare.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "prepare.h"
+#include "launcherdirs.h"
+
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+
+#ifdef VCMI_ANDROID
+#include "../lib/CAndroidVMHelper.h"
+
+#include <QAndroidJniEnvironment>
+#include <QAndroidJniObject>
+#include <QtAndroid>
+
+namespace
+{
+// https://gist.github.com/ssendeavour/7324701
+bool copyRecursively(const QString &srcFilePath, const QString &tgtFilePath)
+{
+	QFileInfo srcFileInfo{srcFilePath};
+	if(srcFileInfo.isDir()) {
+		QDir targetDir{tgtFilePath};
+		targetDir.cdUp();
+		if(!targetDir.mkpath(QFileInfo{tgtFilePath}.fileName()))
+			return false;
+		targetDir.setPath(tgtFilePath);
+
+		QDir sourceDir{srcFilePath};
+		const auto fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
+		for(const auto & fileName : fileNames) {
+			const auto newSrcFilePath = sourceDir.filePath(fileName);
+			const auto newTgtFilePath = targetDir.filePath(fileName);
+			if(!copyRecursively(newSrcFilePath, newTgtFilePath))
+				return false;
+		}
+	} else {
+		if(!QFile::copy(srcFilePath, tgtFilePath))
+			return false;
+	}
+	return true;
+}
+
+void prepareAndroid()
+{
+	QAndroidJniEnvironment jniEnv;
+	CAndroidVMHelper::initClassloader(static_cast<JNIEnv *>(jniEnv));
+
+	const bool justLaunched = QtAndroid::androidActivity().getField<jboolean>("justLaunched") == JNI_TRUE;
+	if(!justLaunched)
+		return;
+
+	// copy core data to internal directory
+	const auto vcmiDir = QAndroidJniObject::callStaticObjectMethod<jstring>("eu/vcmi/vcmi/NativeMethods", "internalDataRoot").toString();
+	for(auto vcmiFilesResource : {QLatin1String{"config"}, QLatin1String{"Mods"}})
+	{
+		QDir destDir = QString{"%1/%2"}.arg(vcmiDir, vcmiFilesResource);
+		destDir.removeRecursively();
+		copyRecursively(QString{":/%1"}.arg(vcmiFilesResource), destDir.absolutePath());
+	}
+}
+}
+#endif
+
+
+namespace launcher
+{
+void prepare()
+{
+#ifdef VCMI_ANDROID
+	prepareAndroid();
+#endif
+
+	CLauncherDirs::prepare();
+}
+}

+ 15 - 0
launcher/prepare.h

@@ -0,0 +1,15 @@
+/*
+ * prepare.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+namespace launcher
+{
+void prepare();
+}

+ 14 - 0
launcher/resources.qrc

@@ -0,0 +1,14 @@
+<RCC>
+    <qresource prefix="/">
+        <file>icons/about-project.png</file>
+        <file>icons/menu-editor.png</file>
+        <file>icons/menu-game.png</file>
+        <file>icons/menu-mods.png</file>
+        <file>icons/menu-settings.png</file>
+        <file>icons/mod-delete.png</file>
+        <file>icons/mod-disabled.png</file>
+        <file>icons/mod-download.png</file>
+        <file>icons/mod-enabled.png</file>
+        <file>icons/mod-update.png</file>
+    </qresource>
+</RCC>

+ 1 - 2
osx/CMakeLists.txt

@@ -7,7 +7,6 @@ if(APPLE_MACOS)
 	if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 		if(USING_CONAN)
 			# simulate macdeployqt behavior, main Qt libs are copied by conan
-			get_target_property(qmakePath Qt5::qmake IMPORTED_LOCATION)
 			execute_process(COMMAND
 				"${qmakePath}" -query QT_INSTALL_PLUGINS
 				OUTPUT_VARIABLE qtPluginsDir
@@ -25,7 +24,7 @@ if(APPLE_MACOS)
 		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 ${qt_base_dir}/bin)
+			find_program(TOOL_MACDEPLOYQT NAMES macdeployqt PATHS "${qtBinDir}")
 			if(TOOL_MACDEPLOYQT)
 				install(CODE "
 					execute_process(COMMAND