From 11c87218c72bcfda9c1ca299369eb2f7dba1f9bd Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:07:07 +0200 Subject: [PATCH 1/8] Added a basic Android APK build --- CMakeLists.txt | 42 ++++++++++--- README.md | 4 ++ android/AndroidManifest.xml | 17 ++++++ android/README.md | 32 ++++++++++ .../org/opengothic/app/GothicActivity.java | 28 +++++++++ common/utils/crashlog.cpp | 8 +-- game/main.cpp | 59 +++++++++++++++++++ shader/CMakeLists.txt | 2 +- 8 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 android/AndroidManifest.xml create mode 100644 android/README.md create mode 100644 android/java/org/opengothic/app/GothicActivity.java diff --git a/CMakeLists.txt b/CMakeLists.txt index 60e6b95b5..3ccfdf4fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,8 +12,12 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_SKIP_RPATH ON) add_library(${PROJECT_NAME} STATIC) -add_executable(Gothic2Notr) -add_executable(Spacer) +if(ANDROID) + add_library(Gothic2Notr SHARED) +else() + add_executable(Gothic2Notr) + add_executable(Spacer) +endif() if(MSVC) add_definitions(-D_USE_MATH_DEFINES) @@ -77,7 +81,9 @@ add_subdirectory(shader) target_link_libraries(${PROJECT_NAME} GothicShaders) # editor icons and such -add_subdirectory(spacer/assets) +if(NOT ANDROID) + add_subdirectory(spacer/assets) +endif() include_directories("common") @@ -92,6 +98,8 @@ endif() if(WIN32) target_link_libraries(${PROJECT_NAME} shlwapi DbgHelp) +elseif(ANDROID) + target_link_libraries(${PROJECT_NAME} dl) elseif(UNIX) target_link_libraries(${PROJECT_NAME} -lpthread -ldl) endif() @@ -178,7 +186,7 @@ if(WIN32) endif() # in debug mode, enable sanitizers -if(${CMAKE_BUILD_TYPE} MATCHES "Debug") +if(${CMAKE_BUILD_TYPE} MATCHES "Debug" AND NOT ANDROID) add_compile_options(-fsanitize=address) add_compile_options(-fsanitize=leak) add_link_options (-fsanitize=address) @@ -197,13 +205,29 @@ if(IOS) XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer") endif() -target_sources(Gothic2Notr PRIVATE game/main.cpp icon.rc) +target_sources(Gothic2Notr PRIVATE game/main.cpp) +if(WIN32) + target_sources(Gothic2Notr PRIVATE icon.rc) +endif() target_link_libraries(Gothic2Notr PRIVATE OpenGothic) # editor -target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp) -target_include_directories(Spacer PRIVATE "spacer") -target_link_libraries(Spacer PRIVATE OpenGothic SpacerAssets) +if(NOT ANDROID) + target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp) + target_include_directories(Spacer PRIVATE "spacer") + target_link_libraries(Spacer PRIVATE OpenGothic SpacerAssets) +endif() + +if(ANDROID) + add_android_apk(OpenGothic-apk + PACKAGE_NAME org.opengothic.app + CODE Gothic2Notr + MANIFEST android/AndroidManifest.xml + JAVA_DIRS android/java) +endif() # installation -install(TARGETS Gothic2Notr Spacer DESTINATION bin) +install(TARGETS Gothic2Notr DESTINATION bin) +if(NOT ANDROID) + install(TARGETS Spacer DESTINATION bin) +endif() diff --git a/README.md b/README.md index b42eb345e..2b2cfb5a7 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ There are ongoing efforts to support parts of it to make at least some popular m - Ninja ## Build Instructions +### Android + +The basic ARM64 APK build and ADB game-file setup are documented in [android/README.md](android/README.md). + ### Linux Install dependencies: * Ubuntu 20.04/22.04 and their derived distros diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 000000000..f666c1e4f --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/android/README.md b/android/README.md new file mode 100644 index 000000000..fd02a4314 --- /dev/null +++ b/android/README.md @@ -0,0 +1,32 @@ +# Android + +An ARM64 NativeActivity APK using Tempest's Android backend and generated Gradle project. This is the basic platform integration; touch controls, controller input and game-file importing are not included yet. Android window destruction closes the application, including when backgrounding or locking destroys its surface. + +## Build + +Install JDK 17, Gradle 8.9, CMake 3.22.1 or newer, Ninja and the Android SDK with SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and SDK CMake 3.22.1. Set `JAVA_HOME`, `ANDROID_HOME` and `VULKAN_SDK`. Put Gradle, Ninja and the host Vulkan SDK's `glslangValidator` on `PATH`. Vulkan headers come from the host SDK; Android links the NDK's Vulkan loader. + +Clone with `git clone --recursive https://github.com/Try/OpenGothic.git`, or run `git submodule update --init --recursive` in an existing checkout. Replace `/path/to/ndk` below with the NDK installation directory. The same commands work in PowerShell and a Linux shell. + +```sh +cmake -S . -B build/android -G Ninja -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 -DCMAKE_BUILD_TYPE=Release +cmake --build build/android --target OpenGothic-apk +``` + +APK: `build/android/OpenGothic-apk/build/outputs/apk/release/OpenGothic-apk-release.apk`. Use `-DCMAKE_BUILD_TYPE=Debug` for a debuggable APK under `outputs/apk/debug/OpenGothic-apk-debug.apk`. Desktop builds do not invoke the APK helper or require Android tools. + +## Install and game files + +Use a legally owned Gothic II: Night of the Raven installation. No game files are included in the APK. Copy the installation root, not only its `Data` directory: `Data`, `_work` and `System` must be directly inside `Gothic2`. + +```sh +adb install -r build/android/OpenGothic-apk/build/outputs/apk/release/OpenGothic-apk-release.apk +adb shell mkdir -p /sdcard/Android/data/org.opengothic.app/files/Gothic2 +adb push "/path/to/Gothic II/." /sdcard/Android/data/org.opengothic.app/files/Gothic2/ +adb shell am start -n org.opengothic.app/.GothicActivity +adb logcat -s OpenGothic AndroidRuntime DEBUG +``` + +Game files use app-specific external storage, without storage permissions. Logs, saves and writable settings use the app's internal files directory. Uninstalling removes both directories. Ray tracing and mesh shading are disabled for this initial Android build. + +With a debug APK, read the file log using `adb shell run-as org.opengothic.app cat files/log.txt`. diff --git a/android/java/org/opengothic/app/GothicActivity.java b/android/java/org/opengothic/app/GothicActivity.java new file mode 100644 index 000000000..e44039fa8 --- /dev/null +++ b/android/java/org/opengothic/app/GothicActivity.java @@ -0,0 +1,28 @@ +package org.opengothic.app; + +import android.app.NativeActivity; +import android.os.Bundle; + +import java.io.File; + +public class GothicActivity extends NativeActivity { + static { + System.loadLibrary("Gothic2Notr"); + } + + private static native void prepareStorage(String writablePath, String gamePath); + + @Override + protected void onCreate(Bundle savedInstanceState) { + File external = getExternalFilesDir(null); + if (external == null) { + throw new IllegalStateException("External application storage is unavailable"); + } + File game = new File(external, "Gothic2"); + if (!game.isDirectory() && !game.mkdirs()) { + throw new IllegalStateException("Unable to create the Gothic2 installation directory"); + } + prepareStorage(getFilesDir().getAbsolutePath(), game.getAbsolutePath()); + super.onCreate(savedInstanceState); + } +} diff --git a/common/utils/crashlog.cpp b/common/utils/crashlog.cpp index 705924315..e2a228e47 100644 --- a/common/utils/crashlog.cpp +++ b/common/utils/crashlog.cpp @@ -14,7 +14,7 @@ #include #endif -#if defined(__LINUX__) || defined(__APPLE__) +#if (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) #include // backtrace #include // dladdr #include // __cxa_demangle @@ -136,7 +136,7 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { #elif defined(__WINDOWS__) traceback.collect(0); traceback.log(db, std::cout); -#elif defined(__LINUX__) || defined(__APPLE__) +#elif (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) tracebackLinux(std::cout); #endif std::cout << std::endl; @@ -150,7 +150,7 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { tracebackStd(fout); #elif defined(__WINDOWS__) traceback.log(db, fout); -#elif defined(__LINUX__) || defined(__APPLE__) +#elif (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) tracebackLinux(fout); #endif fout.flush(); @@ -169,7 +169,7 @@ void CrashLog::tracebackStd(std::ostream &out) { } void CrashLog::tracebackLinux(std::ostream &out) { -#if defined(__LINUX__) || defined(__APPLE__) +#if (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) // inspired by https://gist.github.com/fmela/591333/36faca4c2f68f7483cd0d3a357e8a8dd5f807edf (BSD) void *callstack[64] = {}; char **symbols = nullptr; diff --git a/game/main.cpp b/game/main.cpp index de00118e9..21da45fd6 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -18,6 +18,14 @@ #include "utils/installdetect.h" #endif +#if defined(__ANDROID__) +#include +#include +#include +#include +#include +#endif + #include "utils/crashlog.h" #include "mainwindow.h" #include "gothic.h" @@ -26,6 +34,48 @@ #include +#if defined(__ANDROID__) +static std::string androidGamePath; + +static std::string androidPath(JNIEnv* env, jstring path) { + if(path==nullptr) + throw std::invalid_argument("Missing application storage path"); + const char* text = env->GetStringUTFChars(path,nullptr); + if(text==nullptr) + throw std::runtime_error("Unable to read application storage path"); + try { + std::string result(text); + env->ReleaseStringUTFChars(path,text); + return result; + } + catch(...) { + env->ReleaseStringUTFChars(path,text); + throw; + } + } + +static void throwAndroidError(JNIEnv* env, const char* message) { + if(env->ExceptionCheck()) + return; + jclass type = env->FindClass("java/lang/IllegalStateException"); + if(type!=nullptr) + env->ThrowNew(type,message); + } + +extern "C" JNIEXPORT void JNICALL Java_org_opengothic_app_GothicActivity_prepareStorage(JNIEnv* env, jclass, jstring writablePath, jstring gamePath) { + try { + std::filesystem::current_path(androidPath(env,writablePath)); + androidGamePath = androidPath(env,gamePath); + } + catch(const std::exception& e) { + throwAndroidError(env,e.what()); + } + catch(...) { + throwAndroidError(env,"Unable to initialize application storage"); + } + } +#endif + std::string_view selectDevice(const Tempest::AbstractGraphicsApi& api) { auto d = api.devices(); @@ -68,6 +118,11 @@ std::unique_ptr mkApi(const CommandLine& g) { } int main(int argc,const char** argv) { +#if defined(__ANDROID__) + const char* androidArgs[] = {"Gothic2Notr", "-g", androidGamePath.c_str(), "-rt", "0", "-ms", "0"}; + argc = sizeof(androidArgs)/sizeof(androidArgs[0]); + argv = androidArgs; +#endif #if defined(__IOS__) { auto appdir = InstallDetect::applicationSupportDirectory(); @@ -78,6 +133,10 @@ int main(int argc,const char** argv) { try { static Tempest::WFile logFile("log.txt"); Tempest::Log::setOutputCallback([](Tempest::Log::Mode mode, const char* text) { +#if defined(__ANDROID__) + const int priority = mode==Tempest::Log::Error ? ANDROID_LOG_ERROR : mode==Tempest::Log::Debug ? ANDROID_LOG_DEBUG : ANDROID_LOG_INFO; + __android_log_write(priority,"OpenGothic",text); +#endif logFile.write(text,std::strlen(text)); logFile.write("\n",1); if(mode==Tempest::Log::Error) diff --git a/shader/CMakeLists.txt b/shader/CMakeLists.txt index a3cbe7dfd..a405d3534 100644 --- a/shader/CMakeLists.txt +++ b/shader/CMakeLists.txt @@ -20,7 +20,7 @@ file(GLOB_RECURSE GLSL_SOURCES "${CMAKE_SOURCE_DIR}/shader/*.glsl") # GLSL to SPIRV compiler -find_program(GLSLANGVALIDATOR glslangValidator "/opt/homebrew/bin") +find_program(GLSLANGVALIDATOR glslangValidator "/opt/homebrew/bin" NO_CMAKE_FIND_ROOT_PATH) if(NOT GLSLANGVALIDATOR) message(FATAL_ERROR "glslangValidator required") endif() From ae5587697de525a357ca853aeaeab217ed26c8ab Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:23:06 +0200 Subject: [PATCH 2/8] Addressed Android build integration review --- .github/workflows/build.yml | 44 +++++++++++++++ CMakeLists.txt | 21 +++---- android/AndroidManifest.xml | 4 +- android/README.md | 2 +- .../org/opengothic/app/GothicActivity.java | 28 ---------- common/commandline.h | 2 +- common/utils/crashlog.cpp | 38 ++++++++++++- common/utils/installdetect.cpp | 21 +++++++ common/utils/installdetect.h | 2 +- game/main.cpp | 56 +------------------ lib/Tempest | 2 +- 11 files changed, 116 insertions(+), 104 deletions(-) delete mode 100644 android/java/org/opengothic/app/GothicActivity.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a569a0994..5b1fbca37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,6 +105,50 @@ jobs: name: opengothic_${{ needs.setup.outputs.semver }}_linux_x64.deb path: opengothic_${{ needs.setup.outputs.semver }}_linux_x64.deb + android-arm64: + needs: setup + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: '8.9' + + - uses: ./.github/actions/setup-vulkan + with: + os: Linux + vk-sdk: ${{ env.VK_SDK }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y glslang-tools ninja-build + sdkmanager "platforms;android-35" "build-tools;35.0.0" "ndk;27.0.12077973" "cmake;3.22.1" + + - name: Generate build version + run: | + echo '#pragma once' > game/build.h + echo "static const char* appBuild = \"${{ needs.setup.outputs.version }}\";" >> game/build.h + + - name: Configure + run: | + "$ANDROID_HOME/cmake/3.22.1/bin/cmake" -S . -B build/android -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_HOME/ndk/27.0.12077973/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 \ + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} + + - name: Build APK + run: | + "$ANDROID_HOME/cmake/3.22.1/bin/cmake" --build build/android --target OpenGothic-apk + windows: needs: setup runs-on: windows-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ccfdf4fe..e458f3523 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,8 +16,8 @@ if(ANDROID) add_library(Gothic2Notr SHARED) else() add_executable(Gothic2Notr) - add_executable(Spacer) endif() +add_executable(Spacer) if(MSVC) add_definitions(-D_USE_MATH_DEFINES) @@ -81,9 +81,7 @@ add_subdirectory(shader) target_link_libraries(${PROJECT_NAME} GothicShaders) # editor icons and such -if(NOT ANDROID) - add_subdirectory(spacer/assets) -endif() +add_subdirectory(spacer/assets) include_directories("common") @@ -212,22 +210,17 @@ endif() target_link_libraries(Gothic2Notr PRIVATE OpenGothic) # editor -if(NOT ANDROID) - target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp) - target_include_directories(Spacer PRIVATE "spacer") - target_link_libraries(Spacer PRIVATE OpenGothic SpacerAssets) -endif() +target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp) +target_include_directories(Spacer PRIVATE "spacer") +target_link_libraries(Spacer PRIVATE OpenGothic SpacerAssets) if(ANDROID) add_android_apk(OpenGothic-apk PACKAGE_NAME org.opengothic.app CODE Gothic2Notr - MANIFEST android/AndroidManifest.xml - JAVA_DIRS android/java) + MANIFEST android/AndroidManifest.xml) endif() # installation install(TARGETS Gothic2Notr DESTINATION bin) -if(NOT ANDROID) - install(TARGETS Spacer DESTINATION bin) -endif() +install(TARGETS Spacer DESTINATION bin) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index f666c1e4f..636d7af6a 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,9 +2,9 @@ - - diff --git a/android/README.md b/android/README.md index fd02a4314..c6e3cf2be 100644 --- a/android/README.md +++ b/android/README.md @@ -23,7 +23,7 @@ Use a legally owned Gothic II: Night of the Raven installation. No game files ar adb install -r build/android/OpenGothic-apk/build/outputs/apk/release/OpenGothic-apk-release.apk adb shell mkdir -p /sdcard/Android/data/org.opengothic.app/files/Gothic2 adb push "/path/to/Gothic II/." /sdcard/Android/data/org.opengothic.app/files/Gothic2/ -adb shell am start -n org.opengothic.app/.GothicActivity +adb shell am start -n org.opengothic.app/android.app.NativeActivity adb logcat -s OpenGothic AndroidRuntime DEBUG ``` diff --git a/android/java/org/opengothic/app/GothicActivity.java b/android/java/org/opengothic/app/GothicActivity.java deleted file mode 100644 index e44039fa8..000000000 --- a/android/java/org/opengothic/app/GothicActivity.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.opengothic.app; - -import android.app.NativeActivity; -import android.os.Bundle; - -import java.io.File; - -public class GothicActivity extends NativeActivity { - static { - System.loadLibrary("Gothic2Notr"); - } - - private static native void prepareStorage(String writablePath, String gamePath); - - @Override - protected void onCreate(Bundle savedInstanceState) { - File external = getExternalFilesDir(null); - if (external == null) { - throw new IllegalStateException("External application storage is unavailable"); - } - File game = new File(external, "Gothic2"); - if (!game.isDirectory() && !game.mkdirs()) { - throw new IllegalStateException("Unable to create the Gothic2 installation directory"); - } - prepareStorage(getFilesDir().getAbsolutePath(), game.getAbsolutePath()); - super.onCreate(savedInstanceState); - } -} diff --git a/common/commandline.h b/common/commandline.h index f5532481e..0f6c1130a 100644 --- a/common/commandline.h +++ b/common/commandline.h @@ -64,7 +64,7 @@ class CommandLine { Benchmark isBenchmark = Benchmark::None; bool isWindow = false; bool isDebug = false; -#if defined(__OSX__) +#if defined(__OSX__) || defined(__ANDROID__) bool isRQuery = false; bool isMeshSh = false; #else diff --git a/common/utils/crashlog.cpp b/common/utils/crashlog.cpp index e2a228e47..badeead8b 100644 --- a/common/utils/crashlog.cpp +++ b/common/utils/crashlog.cpp @@ -10,6 +10,12 @@ #include #include +#if defined(__ANDROID__) +#include +#include // dladdr +#include // _Unwind_Backtrace +#endif + #if defined(__cpp_lib_stacktrace) #include #endif @@ -136,7 +142,7 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { #elif defined(__WINDOWS__) traceback.collect(0); traceback.log(db, std::cout); -#elif (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) +#elif defined(__LINUX__) || defined(__ANDROID__) || defined(__APPLE__) tracebackLinux(std::cout); #endif std::cout << std::endl; @@ -150,7 +156,7 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { tracebackStd(fout); #elif defined(__WINDOWS__) traceback.log(db, fout); -#elif (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) +#elif defined(__LINUX__) || defined(__ANDROID__) || defined(__APPLE__) tracebackLinux(fout); #endif fout.flush(); @@ -169,7 +175,33 @@ void CrashLog::tracebackStd(std::ostream &out) { } void CrashLog::tracebackLinux(std::ostream &out) { -#if (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) +#if defined(__ANDROID__) + struct Backtrace { + void* frames[64] = {}; + size_t count = 0; + } trace; + _Unwind_Backtrace([](_Unwind_Context* context, void* arg) { + auto& trace = *static_cast(arg); + const uintptr_t pc = _Unwind_GetIP(context); + if(pc!=0) + trace.frames[trace.count++] = reinterpret_cast(pc); + return trace.count==64 ? _URC_END_OF_STACK : _URC_NO_REASON; + }, &trace); + + for(size_t i=0; i(trace.frames[i])-reinterpret_cast(info.dli_fbase); + out << (info.dli_sname ? info.dli_sname : "?") << " - " << (info.dli_fname ? info.dli_fname : "?"); + out << " + 0x" << std::hex << offset << std::dec; + } + else { + out << trace.frames[i]; + } + out << std::endl; + } +#elif defined(__LINUX__) || defined(__APPLE__) // inspired by https://gist.github.com/fmela/591333/36faca4c2f68f7483cd0d3a357e8a8dd5f807edf (BSD) void *callstack[64] = {}; char **symbols = nullptr; diff --git a/common/utils/installdetect.cpp b/common/utils/installdetect.cpp index 9c7f3de39..7e33a935f 100644 --- a/common/utils/installdetect.cpp +++ b/common/utils/installdetect.cpp @@ -2,6 +2,12 @@ #include +#ifdef __ANDROID__ +#include +#include +#include +#endif + #ifdef __WINDOWS__ #include "windows.h" #include "shlobj.h" @@ -31,11 +37,26 @@ std::u16string InstallDetect::detectG2() { if(FileUtil::exists(appDir)) return appDir; return u""; +#elif defined(__ANDROID__) + auto path = Tempest::AndroidApi::externalDataPath(); + if(path.empty()) + return u""; + path += "/Gothic2"; + std::filesystem::create_directories(path); + return Tempest::TextCodec::toUtf16(path); #else return u""; #endif } +#ifdef __ANDROID__ +std::u16string InstallDetect::applicationSupportDirectory() { + auto path = Tempest::AndroidApi::internalDataPath(); + std::filesystem::create_directories(path); + return Tempest::TextCodec::toUtf16(path); + } +#endif + std::u16string InstallDetect::detectG2(std::u16string pfiles) { if(pfiles.empty()) return u""; diff --git a/common/utils/installdetect.h b/common/utils/installdetect.h index 140855660..81394872d 100644 --- a/common/utils/installdetect.h +++ b/common/utils/installdetect.h @@ -8,7 +8,7 @@ class InstallDetect final { InstallDetect(); std::u16string detectG2(); -#if defined(__OSX__) || defined(__IOS__) +#if defined(__OSX__) || defined(__IOS__) || defined(__ANDROID__) static std::u16string applicationSupportDirectory(); #endif diff --git a/game/main.cpp b/game/main.cpp index 21da45fd6..61a999530 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -14,16 +14,13 @@ #include #endif -#if defined(__IOS__) +#if defined(__IOS__) || defined(__ANDROID__) #include "utils/installdetect.h" +#include #endif #if defined(__ANDROID__) #include -#include -#include -#include -#include #endif #include "utils/crashlog.h" @@ -34,48 +31,6 @@ #include -#if defined(__ANDROID__) -static std::string androidGamePath; - -static std::string androidPath(JNIEnv* env, jstring path) { - if(path==nullptr) - throw std::invalid_argument("Missing application storage path"); - const char* text = env->GetStringUTFChars(path,nullptr); - if(text==nullptr) - throw std::runtime_error("Unable to read application storage path"); - try { - std::string result(text); - env->ReleaseStringUTFChars(path,text); - return result; - } - catch(...) { - env->ReleaseStringUTFChars(path,text); - throw; - } - } - -static void throwAndroidError(JNIEnv* env, const char* message) { - if(env->ExceptionCheck()) - return; - jclass type = env->FindClass("java/lang/IllegalStateException"); - if(type!=nullptr) - env->ThrowNew(type,message); - } - -extern "C" JNIEXPORT void JNICALL Java_org_opengothic_app_GothicActivity_prepareStorage(JNIEnv* env, jclass, jstring writablePath, jstring gamePath) { - try { - std::filesystem::current_path(androidPath(env,writablePath)); - androidGamePath = androidPath(env,gamePath); - } - catch(const std::exception& e) { - throwAndroidError(env,e.what()); - } - catch(...) { - throwAndroidError(env,"Unable to initialize application storage"); - } - } -#endif - std::string_view selectDevice(const Tempest::AbstractGraphicsApi& api) { auto d = api.devices(); @@ -118,12 +73,7 @@ std::unique_ptr mkApi(const CommandLine& g) { } int main(int argc,const char** argv) { -#if defined(__ANDROID__) - const char* androidArgs[] = {"Gothic2Notr", "-g", androidGamePath.c_str(), "-rt", "0", "-ms", "0"}; - argc = sizeof(androidArgs)/sizeof(androidArgs[0]); - argv = androidArgs; -#endif -#if defined(__IOS__) +#if defined(__IOS__) || defined(__ANDROID__) { auto appdir = InstallDetect::applicationSupportDirectory(); std::filesystem::current_path(appdir); diff --git a/lib/Tempest b/lib/Tempest index d83e06fc4..dea473fc7 160000 --- a/lib/Tempest +++ b/lib/Tempest @@ -1 +1 @@ -Subproject commit d83e06fc4d0c1018291af0bf5bb0c1da781eb37d +Subproject commit dea473fc77e054ea7632248fea462a47fe418e50 From 01dbdb1ba76adda24b0810cc9f9c945e087c7558 Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:27:58 +0200 Subject: [PATCH 3/8] Use the hosted SDK manager path --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b1fbca37..a45e2da2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,7 +131,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y glslang-tools ninja-build - sdkmanager "platforms;android-35" "build-tools;35.0.0" "ndk;27.0.12077973" "cmake;3.22.1" + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-35" "build-tools;35.0.0" "ndk;27.0.12077973" "cmake;3.22.1" - name: Generate build version run: | From 714d4a04aacd0e1519309c23136c08cb08d08389 Mon Sep 17 00:00:00 2001 From: Solessfir <34831419+Solessfir@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:46:08 +0200 Subject: [PATCH 4/8] Applied Android review cleanup --- CMakeLists.txt | 3 +-- android/README.md | 2 +- common/utils/crashlog.cpp | 16 ++++++++++++---- common/utils/crashlog.h | 1 + common/utils/installdetect.cpp | 10 ++++------ common/utils/installdetect.h | 8 +++++++- game/main.cpp | 12 +++--------- lib/Tempest | 2 +- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e458f3523..a40e97c17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,5 +222,4 @@ if(ANDROID) endif() # installation -install(TARGETS Gothic2Notr DESTINATION bin) -install(TARGETS Spacer DESTINATION bin) +install(TARGETS Gothic2Notr Spacer DESTINATION bin) diff --git a/android/README.md b/android/README.md index c6e3cf2be..8e81f587e 100644 --- a/android/README.md +++ b/android/README.md @@ -24,7 +24,7 @@ adb install -r build/android/OpenGothic-apk/build/outputs/apk/release/OpenGothic adb shell mkdir -p /sdcard/Android/data/org.opengothic.app/files/Gothic2 adb push "/path/to/Gothic II/." /sdcard/Android/data/org.opengothic.app/files/Gothic2/ adb shell am start -n org.opengothic.app/android.app.NativeActivity -adb logcat -s OpenGothic AndroidRuntime DEBUG +adb logcat -s app AndroidRuntime DEBUG ``` Game files use app-specific external storage, without storage permissions. Logs, saves and writable settings use the app's internal files directory. Uninstalling removes both directories. Ray tracing and mesh shading are disabled for this initial Android build. diff --git a/common/utils/crashlog.cpp b/common/utils/crashlog.cpp index badeead8b..35f2a1468 100644 --- a/common/utils/crashlog.cpp +++ b/common/utils/crashlog.cpp @@ -142,7 +142,9 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { #elif defined(__WINDOWS__) traceback.collect(0); traceback.log(db, std::cout); -#elif defined(__LINUX__) || defined(__ANDROID__) || defined(__APPLE__) +#elif defined(__ANDROID__) + tracebackAndroid(std::cout); +#elif defined(__LINUX__) || defined(__APPLE__) tracebackLinux(std::cout); #endif std::cout << std::endl; @@ -156,7 +158,9 @@ void CrashLog::dumpStack(const char *sig, const char *extGpuLog) { tracebackStd(fout); #elif defined(__WINDOWS__) traceback.log(db, fout); -#elif defined(__LINUX__) || defined(__ANDROID__) || defined(__APPLE__) +#elif defined(__ANDROID__) + tracebackAndroid(fout); +#elif defined(__LINUX__) || defined(__APPLE__) tracebackLinux(fout); #endif fout.flush(); @@ -174,7 +178,7 @@ void CrashLog::tracebackStd(std::ostream &out) { #endif } -void CrashLog::tracebackLinux(std::ostream &out) { +void CrashLog::tracebackAndroid(std::ostream &out) { #if defined(__ANDROID__) struct Backtrace { void* frames[64] = {}; @@ -201,7 +205,11 @@ void CrashLog::tracebackLinux(std::ostream &out) { } out << std::endl; } -#elif defined(__LINUX__) || defined(__APPLE__) +#endif + } + +void CrashLog::tracebackLinux(std::ostream &out) { +#if (defined(__LINUX__) && !defined(__ANDROID__)) || defined(__APPLE__) // inspired by https://gist.github.com/fmela/591333/36faca4c2f68f7483cd0d3a357e8a8dd5f807edf (BSD) void *callstack[64] = {}; char **symbols = nullptr; diff --git a/common/utils/crashlog.h b/common/utils/crashlog.h index 7148a63d4..8cfb84056 100644 --- a/common/utils/crashlog.h +++ b/common/utils/crashlog.h @@ -11,6 +11,7 @@ class CrashLog final { private: static void tracebackStd(std::ostream& out); + static void tracebackAndroid(std::ostream &out); static void tracebackLinux(std::ostream &out); static void tracebackGpu(std::ostream &out, const char* extGpuLog); static void writeSysInfo(std::ostream& fout); diff --git a/common/utils/installdetect.cpp b/common/utils/installdetect.cpp index 7e33a935f..9e5421d9a 100644 --- a/common/utils/installdetect.cpp +++ b/common/utils/installdetect.cpp @@ -4,8 +4,6 @@ #ifdef __ANDROID__ #include -#include -#include #endif #ifdef __WINDOWS__ @@ -41,19 +39,19 @@ std::u16string InstallDetect::detectG2() { auto path = Tempest::AndroidApi::externalDataPath(); if(path.empty()) return u""; - path += "/Gothic2"; + path /= "Gothic2"; std::filesystem::create_directories(path); - return Tempest::TextCodec::toUtf16(path); + return path.u16string(); #else return u""; #endif } #ifdef __ANDROID__ -std::u16string InstallDetect::applicationSupportDirectory() { +std::filesystem::path InstallDetect::androidInternalDataPath() { auto path = Tempest::AndroidApi::internalDataPath(); std::filesystem::create_directories(path); - return Tempest::TextCodec::toUtf16(path); + return path; } #endif diff --git a/common/utils/installdetect.h b/common/utils/installdetect.h index 81394872d..1271ebd23 100644 --- a/common/utils/installdetect.h +++ b/common/utils/installdetect.h @@ -3,13 +3,19 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + class InstallDetect final { public: InstallDetect(); std::u16string detectG2(); -#if defined(__OSX__) || defined(__IOS__) || defined(__ANDROID__) +#if defined(__OSX__) || defined(__IOS__) static std::u16string applicationSupportDirectory(); +#elif defined(__ANDROID__) + static std::filesystem::path androidInternalDataPath(); #endif private: diff --git a/game/main.cpp b/game/main.cpp index 61a999530..82537a8d2 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -19,10 +19,6 @@ #include #endif -#if defined(__ANDROID__) -#include -#endif - #include "utils/crashlog.h" #include "mainwindow.h" #include "gothic.h" @@ -73,7 +69,9 @@ std::unique_ptr mkApi(const CommandLine& g) { } int main(int argc,const char** argv) { -#if defined(__IOS__) || defined(__ANDROID__) +#if defined(__ANDROID__) + std::filesystem::current_path(InstallDetect::androidInternalDataPath()); +#elif defined(__IOS__) { auto appdir = InstallDetect::applicationSupportDirectory(); std::filesystem::current_path(appdir); @@ -83,10 +81,6 @@ int main(int argc,const char** argv) { try { static Tempest::WFile logFile("log.txt"); Tempest::Log::setOutputCallback([](Tempest::Log::Mode mode, const char* text) { -#if defined(__ANDROID__) - const int priority = mode==Tempest::Log::Error ? ANDROID_LOG_ERROR : mode==Tempest::Log::Debug ? ANDROID_LOG_DEBUG : ANDROID_LOG_INFO; - __android_log_write(priority,"OpenGothic",text); -#endif logFile.write(text,std::strlen(text)); logFile.write("\n",1); if(mode==Tempest::Log::Error) diff --git a/lib/Tempest b/lib/Tempest index dea473fc7..6feb8611d 160000 --- a/lib/Tempest +++ b/lib/Tempest @@ -1 +1 @@ -Subproject commit dea473fc77e054ea7632248fea462a47fe418e50 +Subproject commit 6feb8611d2af5c124d98a3d1263e8afb1a7d0fc6 From aefda87b983825bb34dd79f5eb59eb4695cfc5a3 Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 7 Oct 2026 22:16:56 +0200 Subject: [PATCH 5/8] case insensitive path to external files --- common/utils/installdetect.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/utils/installdetect.cpp b/common/utils/installdetect.cpp index 9e5421d9a..1624d795f 100644 --- a/common/utils/installdetect.cpp +++ b/common/utils/installdetect.cpp @@ -36,12 +36,11 @@ std::u16string InstallDetect::detectG2() { return appDir; return u""; #elif defined(__ANDROID__) - auto path = Tempest::AndroidApi::externalDataPath(); + auto path = Tempest::AndroidApi::externalDataPath().u16string(); if(path.empty()) return u""; - path /= "Gothic2"; - std::filesystem::create_directories(path); - return path.u16string(); + path = FileUtil::nestedPath(path + u"/", {u"gothic2"}, Tempest::Dir::FT_Dir); + return path; #else return u""; #endif From 8351fde7f7002eb776bf6c64a29dac09aeb2ceba Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 7 Oct 2026 22:25:04 +0200 Subject: [PATCH 6/8] Update Tempest --- lib/Tempest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Tempest b/lib/Tempest index 6feb8611d..0fde7ca26 160000 --- a/lib/Tempest +++ b/lib/Tempest @@ -1 +1 @@ -Subproject commit 6feb8611d2af5c124d98a3d1263e8afb1a7d0fc6 +Subproject commit 0fde7ca2661e7e83aad6162bf3bf6cdcc04c02c1 From 388461a6e4a1ecdfa9946276092ac882d9d6eba1 Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 7 Oct 2026 22:27:33 +0200 Subject: [PATCH 7/8] Update Tempest --- lib/Tempest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Tempest b/lib/Tempest index 0fde7ca26..d83e06fc4 160000 --- a/lib/Tempest +++ b/lib/Tempest @@ -1 +1 @@ -Subproject commit 0fde7ca2661e7e83aad6162bf3bf6cdcc04c02c1 +Subproject commit d83e06fc4d0c1018291af0bf5bb0c1da781eb37d From e9b320d926036a68463239d5d7d44697266a0890 Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 7 Oct 2026 23:18:48 +0200 Subject: [PATCH 8/8] cleanups --- CMakeLists.txt | 3 ++- README.md | 15 +++++++++++---- game/main.cpp | 5 ++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a40e97c17..24fcd1862 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,10 +14,11 @@ set(CMAKE_SKIP_RPATH ON) add_library(${PROJECT_NAME} STATIC) if(ANDROID) add_library(Gothic2Notr SHARED) + add_library(Spacer SHARED) else() add_executable(Gothic2Notr) + add_executable(Spacer) endif() -add_executable(Spacer) if(MSVC) add_definitions(-D_USE_MATH_DEFINES) diff --git a/README.md b/README.md index 2b2cfb5a7..7062bc9b0 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,6 @@ There are ongoing efforts to support parts of it to make at least some popular m - Ninja ## Build Instructions -### Android - -The basic ARM64 APK build and ADB game-file setup are documented in [android/README.md](android/README.md). - ### Linux Install dependencies: * Ubuntu 20.04/22.04 and their derived distros @@ -151,6 +147,17 @@ cmake --build ./build --target Gothic2Notr ``` Executables can be located at `OpenGothic/build/opengothic`. +### Android +Install dependencies: JDK 17, Gradle 8.9, CMake 3.22.1 or newer, Ninja and the Android SDK with SDK 35, build-tools 35.0.0, NDK 27.0.12077973 and SDK CMake 3.22.1. +Set `JAVA_HOME`, `ANDROID_HOME` and `VULKAN_SDK`. Put Gradle, Ninja and the host Vulkan SDK's `glslangValidator` on `PATH`. +Vulkan headers come from the host SDK; Android links the NDK's Vulkan loader. +```bash +cmake -S . -B build/android -G Ninja -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 -DCMAKE_BUILD_TYPE=Release +cmake --build build/android --target OpenGothic-apk +``` +Details on APK build and ADB game-file setup are documented in [android/README.md](android/README.md). + + ## Video [![Video](https://img.youtube.com/vi/TpayMkyZ58Y/0.jpg)](https://www.youtube.com/watch?v=TpayMkyZ58Y) diff --git a/game/main.cpp b/game/main.cpp index 82537a8d2..dc4d2ddec 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -70,7 +70,10 @@ std::unique_ptr mkApi(const CommandLine& g) { int main(int argc,const char** argv) { #if defined(__ANDROID__) - std::filesystem::current_path(InstallDetect::androidInternalDataPath()); + { + auto appdir = InstallDetect::androidInternalDataPath(); + std::filesystem::current_path(appdir); + } #elif defined(__IOS__) { auto appdir = InstallDetect::applicationSupportDirectory();