diff --git a/.gitignore b/.gitignore index 3f64cf50..7c533c48 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,10 @@ __pycache__/ # Written by 40250 uitaskbar.py (SaveMouseButtonSettings) into the working directory at runtime. /project/mouse.cfg + +# native Android client (./build-android-native.sh): SDL3 download, NDK builds, Gradle output +/build-android-native/ +/android-native/.gradle/ +/android-native/app/build/ +/android-native/build/ +/android-native/local.properties diff --git a/android-native/app/build.gradle b/android-native/app/build.gradle new file mode 100644 index 00000000..01dc2201 --- /dev/null +++ b/android-native/app/build.gradle @@ -0,0 +1,39 @@ +// SDLActivity shell around native_render (libmain.so). The native libraries, the SDL Java sources +// and the assets come from ../build-android-native, produced by ../build-android-native.sh. +plugins { + id 'com.android.application' +} + +def nativeBuild = file("${rootDir}/../build-android-native") + +android { + namespace = 'org.metin2port.client' + compileSdk 35 + defaultConfig { + applicationId 'org.metin2port.client' + minSdk 24 + targetSdk 35 + versionCode 1 + versionName '0.1' + ndk { abiFilters 'arm64-v8a' } + } + buildTypes { + release { + minifyEnabled false + signingConfig signingConfigs.debug + } + } + sourceSets.main { + java.srcDirs += "${nativeBuild}/_deps/SDL3-3.4.16/android-project/app/src/main/java" + jniLibs.srcDirs = ["${nativeBuild}/jniLibs"] + assets.srcDirs = ["${nativeBuild}/app-build/native_render/android-assets"] + } + packagingOptions { + jniLibs { useLegacyPackaging = true } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + lint { abortOnError = false } +} diff --git a/android-native/app/src/main/AndroidManifest.xml b/android-native/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..55ee99cb --- /dev/null +++ b/android-native/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/android-native/app/src/main/java/org/metin2port/client/MainActivity.java b/android-native/app/src/main/java/org/metin2port/client/MainActivity.java new file mode 100644 index 00000000..764f3497 --- /dev/null +++ b/android-native/app/src/main/java/org/metin2port/client/MainActivity.java @@ -0,0 +1,27 @@ +package org.metin2port.client; + +import android.content.Intent; + +import org.libsdl.app.SDLActivity; + +/** + * Runs native_render's main() (libmain.so). Command-line arguments come from the launch intent, + * e.g. adb shell am start -n org.metin2port.client/.MainActivity \ + * --es args "--live-server 192.168.21.203:11000:13000 --login-screen" + * Without arguments the client runs against the in-process fake server once Client/ is in files/. + */ +public class MainActivity extends SDLActivity { + @Override + protected String[] getLibraries() { + return new String[] { "SDL3", "main" }; + } + + @Override + protected String[] getArguments() { + Intent intent = getIntent(); + String args = intent != null ? intent.getStringExtra("args") : null; + if (args == null || args.trim().isEmpty()) + return new String[0]; + return args.trim().split("\\s+"); + } +} diff --git a/android-native/app/src/main/res/values/strings.xml b/android-native/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..515878cb --- /dev/null +++ b/android-native/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Metin2 + diff --git a/android-native/build.gradle b/android-native/build.gradle new file mode 100644 index 00000000..5b7ccba7 --- /dev/null +++ b/android-native/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/android-native/gradle.properties b/android-native/gradle.properties new file mode 100644 index 00000000..d24c4a10 --- /dev/null +++ b/android-native/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2048m +android.useAndroidX=false diff --git a/android-native/gradle/wrapper/gradle-wrapper.jar b/android-native/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..2b338a93 Binary files /dev/null and b/android-native/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-native/gradle/wrapper/gradle-wrapper.properties b/android-native/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b5c11a6f --- /dev/null +++ b/android-native/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/android-native/gradlew b/android-native/gradlew new file mode 100755 index 00000000..3427607f --- /dev/null +++ b/android-native/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android-native/settings.gradle b/android-native/settings.gradle new file mode 100644 index 00000000..65b4096d --- /dev/null +++ b/android-native/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} +rootProject.name = 'metin2-native' +include ':app' diff --git a/build-android-native.sh b/build-android-native.sh new file mode 100755 index 00000000..ba2d5e85 --- /dev/null +++ b/build-android-native.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Build the native (SDL3 + Vulkan) 40250 client as an Android arm64 APK. +# ./build-android-native.sh [Debug|Release] [--install] +# Output: android-native/app/build/outputs/apk/debug/app-debug.apk +# The 1.4 GB Client/ directory is not in the APK; push it once with ./push-android-client.sh. +set -euo pipefail +cd "$(dirname "$0")" +REPO="$PWD" + +CONFIG=Release +INSTALL=0 +for a in "$@"; do + case "$a" in + Debug|Release) CONFIG="$a" ;; + --install) INSTALL=1 ;; + *) echo "usage: $0 [Debug|Release] [--install]" >&2; exit 2 ;; + esac +done +API=24 +OUT="$REPO/build-android-native" +JOBS="${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}" +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}}" + +# --- NDK: $ANDROID_NDK_HOME, else the installed 27.x, else the newest --- +NDK="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}" +if [ -z "$NDK" ] && [ -d "$SDK/ndk" ]; then + v=$(ls -1 "$SDK/ndk" | grep '^27\.' | sort -V | tail -1 || true) + [ -n "$v" ] || v=$(ls -1 "$SDK/ndk" | sort -V | tail -1) + NDK="$SDK/ndk/$v" +fi +[ -f "$NDK/build/cmake/android.toolchain.cmake" ] || { echo "Android NDK not found (set ANDROID_NDK_HOME)" >&2; exit 1; } +echo "NDK: $NDK" +TOOLCHAIN="$NDK/build/cmake/android.toolchain.cmake" +GLSLC="${GLSLC:-$(command -v glslc || true)}" +[ -n "$GLSLC" ] || { echo "glslc not found (brew install shaderc)" >&2; exit 1; } + +# --- SDL3 source (Java SDLActivity + libSDL3.so), same release as the macOS build --- +SDL_VER=3.4.16 +SDL_SHA256=7322236cd12090c3eb40b9728be4d49c76f66ad17d04369584d4ecad5cf77c68 +SDL_SRC="$OUT/_deps/SDL3-$SDL_VER" +if [ ! -f "$SDL_SRC/CMakeLists.txt" ]; then + mkdir -p "$OUT/_deps" + tarball="$OUT/_deps/SDL3-$SDL_VER.tar.gz" + curl -fL -o "$tarball" "https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VER/SDL3-$SDL_VER.tar.gz" + echo "$SDL_SHA256 $tarball" | shasum -a 256 -c - + tar xzf "$tarball" -C "$OUT/_deps" +fi +if [ ! -f "$OUT/sdl3/lib/libSDL3.so" ]; then + cmake -S "$SDL_SRC" -B "$OUT/sdl3-build" -G "Unix Makefiles" \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM="android-$API" \ + -DCMAKE_BUILD_TYPE=Release -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TEST_LIBRARY=OFF \ + -DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX="$OUT/sdl3" + cmake --build "$OUT/sdl3-build" -j "$JOBS" + cmake --install "$OUT/sdl3-build" +fi + +# --- libmain.so (native_render + port_platform + embedded Python) and its assets --- +cmake -S . -B "$OUT/app-build" -G "Unix Makefiles" -Wno-deprecated \ + -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM="android-$API" \ + -DCMAKE_BUILD_TYPE="$CONFIG" -DMTGODOT_BUILD_TOOLS=OFF -DBUILD_TESTING=OFF \ + -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DSDL3_DIR="$OUT/sdl3/lib/cmake/SDL3" \ + -DCMAKE_FIND_ROOT_PATH="$OUT/sdl3" -DGLSLC="$GLSLC" >/dev/null +cmake --build "$OUT/app-build" --target main mt_native_android_assets -j "$JOBS" + +mkdir -p "$OUT/jniLibs/arm64-v8a" +cp -f "$OUT/sdl3/lib/libSDL3.so" "$OUT/app-build/native_render/libmain.so" "$OUT/jniLibs/arm64-v8a/" +STRIP=$(ls -1d "$NDK"/toolchains/llvm/prebuilt/*/bin/llvm-strip | head -1) +"$STRIP" --strip-unneeded "$OUT/jniLibs/arm64-v8a/libSDL3.so" "$OUT/jniLibs/arm64-v8a/libmain.so" + +# --- APK --- +if [ -z "${JAVA_HOME:-}" ]; then + for j in /opt/homebrew/opt/openjdk@21 /opt/homebrew/opt/openjdk@17; do + [ -x "$j/bin/java" ] && { export JAVA_HOME="$j"; break; } + done +fi +export ANDROID_HOME="$SDK" +variant=Debug # debug-signed either way (sideload); CONFIG picks the native optimisation level +(cd android-native && ./gradlew --quiet "assemble$variant") +APK="$REPO/android-native/app/build/outputs/apk/debug/app-debug.apk" +ls -la "$APK" + +if [ "$INSTALL" = 1 ]; then + adb install -r "$APK" +fi diff --git a/extension/src/port/CMakeLists.txt b/extension/src/port/CMakeLists.txt index 406a2cd6..1fb3705d 100644 --- a/extension/src/port/CMakeLists.txt +++ b/extension/src/port/CMakeLists.txt @@ -47,6 +47,11 @@ if(NOT MSVC) set(MT_PORT_WARNING_FLAGS -Wno-unknown-pragmas -Wno-format-security) endif() target_compile_options(port_logic PUBLIC ${MT_PORT_WARNING_FLAGS}) +# 40250 headers keep `#pragma comment(lib, "d3d8.lib")` etc. verbatim; clang turns them into ELF +# .deplibs that lld then fails to resolve (ld64 on Apple ignores unknown ones). +if(CMAKE_SYSTEM_NAME MATCHES "Android|Linux") + target_link_options(port_logic INTERFACE LINKER:--no-dependent-libraries) +endif() target_compile_features(port_logic PUBLIC cxx_std_20) # Armadillo nanomite markers (StdAfx.h `__asm _emit` jumps over a signature): x86-32 MSVC inline asm that no # target here accepts, and a no-op without the Armadillo wrapper, so they expand to nothing. diff --git a/native_render/README.md b/native_render/README.md index 15b59cd2..0b2b7542 100644 --- a/native_render/README.md +++ b/native_render/README.md @@ -32,21 +32,28 @@ returned by SDL's `SDL_GetBasePath`). `MT_PYTHON_STDLIB` can override the zip pa ## Android integration -With an Android toolchain and SDL3 Android AAR/Prefab available to CMake, the -same option builds `libmain.so` for SDLActivity. The `mt_native_android_assets` -target prepares `native.vert.spv`, `native.frag.spv`, and `python27.zip` under -`/native_render/android-assets` (`python27.zip` is included when the -embedded Python target is enabled); include that directory in the SDL app's -`assets.srcDirs`. The app must allow network access for login. The 40250 `Client` -directory must be placed at `SDL_GetPrefPath("mtgodot", "native-render")/Client`, -with `pack/Index` present, before live mode starts. The Godot APK is a separate -application and does not launch this SDL renderer. +`./build-android-native.sh [Debug|Release] [--install]` builds the arm64 APK +(`android-native/app/build/outputs/apk/debug/app-debug.apk`, package +`org.metin2port.client`): it downloads SDL 3.4.16 (sha256-checked) into +`build-android-native/_deps`, builds `libSDL3.so`, cross-builds `libmain.so` +(this target + `port_platform` + embedded Python) at API 24, and wraps both in the +SDLActivity shell under `android-native/` (`MainActivity` passes the launch intent's +`args` extra as argv). `mt_native_android_assets` supplies `native.vert.spv`, +`native.frag.spv` and `python27.zip` as APK assets. -The standalone `arm64-v8a` renderer cross-builds at Android API 24. The full -live-client cross-build currently stops in `extension/src/port/common/Win32Crt.cpp`: -the NDK exposes `` at API 24 but does not declare `iconv` until API 28. -This is a port-runtime prerequisite for an Android live-client APK; raising the -minimum API level is not assumed here. +The 40250 `Client` directory is not packaged. `./push-android-client.sh` copies it +to the app's `files/Client` (`SDL_GetPrefPath`) with `run-as`, so it needs the +debug-signed APK installed first. Without arguments the app runs against the +in-process fake server; for a real server: + +```sh +adb shell am start -n org.metin2port.client/.MainActivity \ + --es args "--live-server HOST:AUTH_PORT:GAME_PORT --login-screen" +``` + +Code page conversion does not use iconv: `extension/src/codepage` carries Microsoft's +WindowsBestFit tables for 874/932/936/949/950/1250-1258 (regenerate with +`python3 script/gen_codepage_tables.py`), so Android converts the same as macOS. The Vulkan portability enumeration extension is selected only when advertised by the loader. DDS, TGA, and memory textures retain their existing decoders; diff --git a/push-android-client.sh b/push-android-client.sh new file mode 100755 index 00000000..7c9eb7c4 --- /dev/null +++ b/push-android-client.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Copy the 40250 Client/ directory into the Android app's private files/ (SDL_GetPrefPath), where +# native_render looks for files/Client/pack/Index. Needs the debug APK installed (run-as). +# ./push-android-client.sh [--locale XX] [CLIENT_DIR] +# --locale XX writes the pushed locale.cfg from CLIENT_DIR/locale_XX.cfg. --locale zh defaults +# CLIENT_DIR to build/zh-locale/Client (script/build_zh_locale.py), whose pack/ registers locale_zh. +# Symbolic links in CLIENT_DIR (the zh client links the original packs) are copied as their targets. +set -euo pipefail +cd "$(dirname "$0")" +PKG=org.metin2port.client +LOCALE="" +if [ "${1:-}" = "--locale" ]; then + LOCALE="${2:?--locale needs a locale code}" + shift 2 +fi +if [ "$LOCALE" = zh ]; then + CLIENT="${1:-build/zh-locale/Client}" +else + CLIENT="${1:-Client}" +fi +[ -f "$CLIENT/pack/Index" ] || { echo "$CLIENT/pack/Index not found" >&2; exit 1; } +# The archive's top directory is basename(CLIENT DIR); the app reads files/Client. +[ "$(basename "$CLIENT")" = Client ] || { echo "CLIENT_DIR must be named Client" >&2; exit 1; } +if [ -n "$LOCALE" ]; then + [ -f "$CLIENT/locale_$LOCALE.cfg" ] || { echo "$CLIENT/locale_$LOCALE.cfg not found" >&2; exit 1; } + pack="$(cut -d' ' -f3 < "$CLIENT/locale_$LOCALE.cfg" | tr -d '\r\n')" + tr -d '\r' < "$CLIENT/pack/Index" | grep -qxF "locale_$pack" || + { echo "locale_$pack is not registered in $CLIENT/pack/Index" >&2; exit 1; } +fi +TMP=/data/local/tmp/mt-client.tar +echo "packing $CLIENT ..." +tar -h -C "$(dirname "$CLIENT")" -cf - "$(basename "$CLIENT")" | adb shell "cat > $TMP" +echo "unpacking into $PKG files/ ..." +adb shell "run-as $PKG sh -c 'mkdir -p files && rm -rf files/Client && cd files && tar -xf $TMP' && rm -f $TMP" +if [ -n "$LOCALE" ]; then + adb shell "run-as $PKG sh -c 'cp files/Client/locale_$LOCALE.cfg files/Client/locale.cfg'" + echo "locale.cfg: $(adb shell "run-as $PKG cat files/Client/locale.cfg")" +fi +adb shell "run-as $PKG ls files/Client/pack/Index" >/dev/null && echo "done: files/Client"