From 9ba658a132174f9fcd3f19a9767e3f94298e4c1d Mon Sep 17 00:00:00 2001 From: Bruce Ashton Date: Mon, 28 Sep 2026 22:24:08 +1300 Subject: [PATCH 1/5] Android: fix menu selection and prompt handling on 64-bit devices * Menu identifiers were passed to Java as ident->a_int and rebuilt from that int in select_menu, leaving the upper half of the anything union uninitialized. Many core menus (pickup, loot, multi-drop, ...) store object pointers as identifiers, so on a 64-bit build selecting an item hands the game a truncated pointer and crashes it. Keep the identifiers in a per-window table on the C side and pass Java a 1-based index instead, 0 still meaning "not selectable". ForkFront only uses the identifier as an opaque value, so no Java changes are needed. The table is reset by start_menu and freed by destroy_nhwindow. * and_yn_function computed the offset of ESC in the choices string by casting both pointers to int. Subtract the pointers instead. Co-Authored-By: Claude Opus 5.5 --- sys/android/winandroid.c | 70 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/sys/android/winandroid.c b/sys/android/winandroid.c index 5c6caa769f..7a6512bce0 100644 --- a/sys/android/winandroid.c +++ b/sys/android/winandroid.c @@ -173,6 +173,16 @@ static char* msghistory[32]; static int msghistory_idx; static int msghistory_idx0; +// Menu identifiers are often pointers, which don't fit in the int passed to +// Java on 64-bit devices. Keep them here and pass Java a 1-based index. +struct menu_idents { + winid wid; + anything *ids; + int n, size; + struct menu_idents *next; +}; +static struct menu_idents *menu_idents; + extern const char *status_fieldfmt[MAXBLSTATS]; // Need to separate conditions in order to color them properly enum bl_conditions { @@ -632,6 +642,17 @@ void and_display_nhwindow(winid wid, BOOLEAN_P blocking) // already been dismissed. void and_destroy_nhwindow(winid wid) { + struct menu_idents **pm, *m; + for(pm = &menu_idents; (m = *pm) != 0; pm = &m->next) + { + if(m->wid == wid) + { + *pm = m->next; + free(m->ids); + free(m); + break; + } + } JNICallV(jDestroyWindow, wid); } @@ -1145,8 +1166,24 @@ void and_display_file(const char *name, BOOLEAN_P complain) // before add_menu(). After calling start_menu() you may not // putstr() to the window. Only windows of type NHW_MENU may // be used for menus. +static struct menu_idents *get_menu_idents(winid wid) +{ + struct menu_idents *m; + for(m = menu_idents; m; m = m->next) + if(m->wid == wid) + return m; + m = (struct menu_idents*)alloc(sizeof(struct menu_idents)); + m->wid = wid; + m->ids = 0; + m->n = m->size = 0; + m->next = menu_idents; + menu_idents = m; + return m; +} + void and_start_menu(winid wid) { + get_menu_idents(wid)->n = 0; JNICallV(jStartMenu, wid); } @@ -1185,6 +1222,24 @@ void and_start_menu(winid wid) void and_add_menu(winid wid, int glyph, const ANY_P *ident, CHAR_P accelerator, CHAR_P groupacc, int attr, const char *str, BOOLEAN_P preselected) { int tile, color; + int id = 0; // 0 means not selectable + if(ident->a_void) + { + struct menu_idents *m = get_menu_idents(wid); + if(m->n == m->size) + { + anything *ids; + m->size = m->size ? m->size * 2 : 32; + ids = (anything*)alloc(sizeof(anything) * m->size); + if(m->n) + memcpy(ids, m->ids, sizeof(anything) * m->n); + free(m->ids); + m->ids = ids; + } + m->ids[m->n++] = *ident; + id = m->n; + } + if(glyph == NO_GLYPH) tile = -1; else @@ -1202,7 +1257,7 @@ void and_add_menu(winid wid, int glyph, const ANY_P *ident, CHAR_P accelerator, attr = 1<a_int, (int)accelerator, (int)groupacc, attr, jstr, (int)preselected, color); + JNICallV(jAddMenu, wid, tile, id, (int)accelerator, (int)groupacc, attr, jstr, (int)preselected, color); destroy_jobject(jstr); } @@ -1272,11 +1327,16 @@ int and_select_menu_r(winid wid, int how, MENU_ITEM_P **selected, int reentry) { n >>= 1; + struct menu_idents *m = get_menu_idents(wid); q = p = (*jEnv)->GetIntArrayElements(jEnv, a, 0); *selected = (MENU_ITEM_P*)malloc(sizeof(MENU_ITEM_P) * n); for(i = 0; i < n; i++) { - (*selected)[i].item.a_int = *p++; + int id = *p++; + if(id > 0 && id <= m->n) + (*selected)[i].item = m->ids[id - 1]; + else + (*selected)[i].item = zeroany; (*selected)[i].count = *p++; } (*jEnv)->ReleaseIntArrayElements(jEnv, a, q, 0); @@ -1575,9 +1635,9 @@ char and_yn_function(const char *question, const char *choices, CHAR_P def) if(choices) { nChoices = strlen(choices); - esc = (int)index(choices, '\033'); - if(esc) - esc -= (int)choices; + const char *pesc = index(choices, '\033'); + if(pesc) + esc = (int)(pesc - choices); else esc = -1; } From 60b414b76a113bd5c2ca81ef4975218a7d430a8d Mon Sep 17 00:00:00 2001 From: Bruce Ashton Date: Mon, 28 Sep 2026 22:24:27 +1300 Subject: [PATCH 2/5] Android: build for arm64-v8a by default The native library was only built for armeabi (32-bit ARM). Devices with 64-bit-only CPUs, such as the Galaxy Z Fold6 (Snapdragon 8 Gen 3) and Pixel 7 and later, cannot load it, so the app fails to install or start. * Add an ABI variable to Makefile.top, defaulting to arm64-v8a and exported to the sub-makes. Override with 'make install ABI=...'. * Build the host utilities (lev_comp, dgn_comp, ...) with -m32 only for 32-bit ABIs. The compiled data files contain longs and version_info structs whose layout depends on word size, so they must match the target. Previously the utilities were always built with -m32. As a consequence, one set of data files only works with one ABI, so an APK supports exactly the ABI it was built for. Saved games are not compatible between 32-bit and 64-bit builds either. * Only pass -Wl,--fix-cortex-a8 for 32-bit ARM, it is not valid for other targets. Link 64-bit libraries with -Wl,-z,max-page-size=16384 for devices using 16 KB memory pages. * Remove app/libs before linking so a library left over from a build for another ABI is not packaged together with mismatched data files. * Build arm64-v8a and armeabi APKs in the GitHub Actions workflow and name the artifacts after the ABI. * Document ABI selection in README.md. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/android.yml | 9 ++++++--- sys/android/Makefile.src | 17 ++++++++++------- sys/android/Makefile.top | 7 +++++++ sys/android/Makefile.utl | 14 ++++++++++++-- sys/android/README.md | 10 ++++++++++ 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index fe62a28042..54f448eb30 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -6,6 +6,9 @@ permissions: jobs: build: runs-on: ubuntu-24.04 + strategy: + matrix: + abi: [arm64-v8a, armeabi] steps: - uses: actions/checkout@v6 - name: Install required packages @@ -23,14 +26,14 @@ jobs: - name: Configure path to NDK run: sed -i -e 's,NDK = .*,NDK = /usr/local/lib/android/sdk/ndk/21.4.7075529,g' sys/android/Makefile.src - name: Build native Android application - run: cd sys/android && sh ./setup.sh && cd ../.. && make install + run: cd sys/android && sh ./setup.sh && cd ../.. && make install ABI=${{ matrix.abi }} - name: Build NetHack run: cd sys/android && sh gradlew build - uses: actions/upload-artifact@v5 with: - name: nethack-release-unsigned + name: nethack-release-unsigned-${{ matrix.abi }} path: sys/android/app/build/outputs/apk/release/app-release-unsigned.apk - uses: actions/upload-artifact@v5 with: - name: nethack-debug + name: nethack-debug-${{ matrix.abi }} path: sys/android/app/build/outputs/apk/debug/app-debug.apk diff --git a/sys/android/Makefile.src b/sys/android/Makefile.src index e98bbbce2f..f06c9bbe99 100644 --- a/sys/android/Makefile.src +++ b/sys/android/Makefile.src @@ -91,12 +91,8 @@ NDK = /path/to/android-ndk-r21e NDK_TOOLCHAIN = $(NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin -# Choose an ABI -ABI = armeabi -#ABI = armeabi-v7a -#ABI = arm64-v8a -#ABI = x86 -#ABI = x86_64 +# Choose an ABI (normally set in the top-level Makefile) +ABI ?= arm64-v8a CC_armeabi = $(NDK_TOOLCHAIN)/armv7a-linux-androideabi30-clang LD_armeabi = $(NDK_TOOLCHAIN)/arm-linux-androideabi-ld @@ -118,7 +114,13 @@ LINK := $(CC) # Compile normally CFLAGS = -DANDROID -O2 -g -Wno-format -fsigned-char -fPIC -I../include #CFLAGS = -DANDROID -DDEBUG -g -Wno-format -fsigned-char -fPIC -I../include -LFLAGS = -shared -Wl,--fix-cortex-a8 +LFLAGS_armeabi = -Wl,--fix-cortex-a8 +LFLAGS_armeabi-v7a = -Wl,--fix-cortex-a8 +# 16 KB page alignment, required by newer 64-bit Android devices +LFLAGS_arm64-v8a = -Wl,-z,max-page-size=16384 +LFLAGS_x86_64 = -Wl,-z,max-page-size=16384 +LFLAGS_VAR = LFLAGS_$(ABI) +LFLAGS = -shared $($(LFLAGS_VAR)) # LFLAGS = -L/usr/X11R6/lib # OR compile backwards compatible a.out format @@ -408,6 +410,7 @@ Sysunix: $(HOBJ) Makefile Sysandroid: $(HOBJ) Makefile @echo "Loading ..." + rm -rf ../sys/android/app/libs mkdir -p ../sys/android/app/libs/$(ABI) $(LINK) $(LFLAGS) -o ../sys/android/app/libs/$(ABI)/lib$(GAME).so $(HOBJ) $(LIBS) @touch Sysandroid diff --git a/sys/android/Makefile.top b/sys/android/Makefile.top index 20df4a4461..9ee70663cf 100644 --- a/sys/android/Makefile.top +++ b/sys/android/Makefile.top @@ -19,6 +19,13 @@ NHSROOT=. # make NetHack #PREFIX = /usr GAME = nethack + +# Target ABI for the native library: arm64-v8a, armeabi, armeabi-v7a, x86 +# or x86_64. Override with 'make install ABI=...'. The compiled data files +# depend on the target's word size, so the host utilities are built to match +# and one build of the data files only works with one ABI. +ABI = arm64-v8a +export ABI # GAME = nethack.prg #GAMEUID = games #GAMEGRP = bin diff --git a/sys/android/Makefile.utl b/sys/android/Makefile.utl index f11e3de35a..9d969799f9 100644 --- a/sys/android/Makefile.utl +++ b/sys/android/Makefile.utl @@ -92,8 +92,18 @@ NHSROOT=.. # flags for debugging: # CFLAGS = -g -I../include -CFLAGS = -DANDROID -m32 -Wno-format -fsigned-char -I../include -LFLAGS = -m32 +# The compiled data files must match the target's word size, so build the +# host utilities as 32-bit for 32-bit ABIs (needs gcc-multilib) and 64-bit +# otherwise. +ABI ?= arm64-v8a +HOSTBITS_armeabi = -m32 +HOSTBITS_armeabi-v7a = -m32 +HOSTBITS_x86 = -m32 +HOSTBITS_VAR = HOSTBITS_$(ABI) +HOSTBITS = $($(HOSTBITS_VAR)) + +CFLAGS = -DANDROID $(HOSTBITS) -Wno-format -fsigned-char -I../include +LFLAGS = $(HOSTBITS) LIBS = diff --git a/sys/android/README.md b/sys/android/README.md index ed554f8b5d..0cb9a04231 100644 --- a/sys/android/README.md +++ b/sys/android/README.md @@ -32,6 +32,16 @@ work. If you're running Windows you're on your own. 4. `cd ../..` 5. `make install` +This builds for `arm64-v8a`, which works on all current devices. Some newer +devices, e.g. the Galaxy Z Fold6 or Pixel 7 and later, can only run 64-bit code. +To build for another ABI, e.g. older 32-bit devices, pass it to make: +`make install ABI=armeabi-v7a`. The 32-bit ABIs need `gcc-multilib`. + +The compiled data files depend on the target's word size, so an APK only +supports the one ABI it was built for. Run `make spotless` before switching +ABI. Saved games from a 32-bit build can't be loaded by a 64-bit build, and +the reverse is also true. + ### Build the Android application 1. `cd /path/to/NetHack-Android/sys/android` From 9478ef2b3d20c8c8538b171a567ac969555a671b Mon Sep 17 00:00:00 2001 From: Bruce Ashton Date: Mon, 28 Sep 2026 22:24:36 +1300 Subject: [PATCH 3/5] Android: upgrade NDK to r27d Recommend and use NDK r27d (27.3.13750724), the current LTS release, in place of r21e. Newer NDKs are needed to keep up with Android platform requirements such as 16 KB page size support. * Update the example NDK path in Makefile.src, the sdkmanager command in README.md and the NDK installed by the GitHub Actions workflow. * Remove the unused LD_* variables. They pointed at the GNU linkers, which were removed in NDK r22; linking has always gone through clang. The compiler names are unchanged and both arm64-v8a and armeabi build without errors with clang 18. The remaining warnings are about K&R-style function definitions and are harmless with the default C17 standard. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/android.yml | 4 ++-- sys/android/Makefile.src | 8 +------- sys/android/README.md | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 54f448eb30..04e4d8bcb3 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,9 +22,9 @@ jobs: uses: android-actions/setup-android@v3 with: cmdline-tools-version: 9123335 - packages: 'platforms;android-30 ndk;21.4.7075529' + packages: 'platforms;android-30 ndk;27.3.13750724' - name: Configure path to NDK - run: sed -i -e 's,NDK = .*,NDK = /usr/local/lib/android/sdk/ndk/21.4.7075529,g' sys/android/Makefile.src + run: sed -i -e 's,NDK = .*,NDK = /usr/local/lib/android/sdk/ndk/27.3.13750724,g' sys/android/Makefile.src - name: Build native Android application run: cd sys/android && sh ./setup.sh && cd ../.. && make install ABI=${{ matrix.abi }} - name: Build NetHack diff --git a/sys/android/Makefile.src b/sys/android/Makefile.src index f06c9bbe99..6abd99d153 100644 --- a/sys/android/Makefile.src +++ b/sys/android/Makefile.src @@ -87,7 +87,7 @@ SYSOBJ = ioctl.o androidmain.o unixtty.o androidunix.o # CC = arm-linux-gcc # # For cross-compiling with Android NDK -NDK = /path/to/android-ndk-r21e +NDK = /path/to/android-ndk-r27d NDK_TOOLCHAIN = $(NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin @@ -95,18 +95,12 @@ NDK_TOOLCHAIN = $(NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin ABI ?= arm64-v8a CC_armeabi = $(NDK_TOOLCHAIN)/armv7a-linux-androideabi30-clang -LD_armeabi = $(NDK_TOOLCHAIN)/arm-linux-androideabi-ld CC_armeabi-v7a = $(NDK_TOOLCHAIN)/armv7a-linux-androideabi30-clang -LD_armeabi-v7a = $(NDK_TOOLCHAIN)/arm-linux-androideabi-ld CC_arm64-v8a = $(NDK_TOOLCHAIN)/aarch64-linux-android30-clang -LD_arm64-v8a = $(NDK_TOOLCHAIN)/aarch64-linux-android-ld CC_x86 = $(NDK_TOOLCHAIN)/i686-linux-android30-clang -LD_x86 = $(NDK_TOOLCHAIN)/i686-linux-android-ld CC_x86_64 = $(NDK_TOOLCHAIN)/x86_64-linux-android30-clang -LD_x86_64 = $(NDK_TOOLCHAIN)/x86_64-linux-android-ld CC_VAR = CC_$(ABI) -LD_VAR = LD_$(ABI) CC := $($(CC_VAR)) LINK := $(CC) diff --git a/sys/android/README.md b/sys/android/README.md index 0cb9a04231..5dcf3a8f5c 100644 --- a/sys/android/README.md +++ b/sys/android/README.md @@ -19,7 +19,7 @@ work. If you're running Windows you're on your own. 1. `cd /path/to/android-sdk/tools/bin` 2. Update the sdk manager: `./sdkmanager --update`. If you get "NoClassDefFoundError" it's because you're not running JDK 8. Make sure the env variable `JAVA_HOME` points to JDK 8. 3. Install the platform tools: `./sdkmanager --install "platforms;android-30"` - 4. Install the NDK: `./sdkmanager --install "ndk;21.4.7075529"` + 4. Install the NDK: `./sdkmanager --install "ndk;27.3.13750724"` ## Build From 5eb56149c33eca0877327b9ab5760e394b93cceb Mon Sep 17 00:00:00 2001 From: Bruce Ashton Date: Mon, 28 Sep 2026 22:27:15 +1300 Subject: [PATCH 4/5] Android: fix .gitignore paths for build output The native library and game data are written to app/libs and app/assets/nethackdir since the Gradle restructuring, but .gitignore still listed the old libs/* and assets/nethackdir locations, so both showed up as untracked after every build. Co-Authored-By: Claude Opus 5.5 --- sys/android/.gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sys/android/.gitignore b/sys/android/.gitignore index 6959d1aef3..8b7aecc5f5 100644 --- a/sys/android/.gitignore +++ b/sys/android/.gitignore @@ -1,6 +1,6 @@ bin -libs/* -assets/nethackdir +app/libs/ +app/assets/nethackdir/ gen .gradle build From 8c746a0bd78324689b2546ed63b0fd1a42a651ea Mon Sep 17 00:00:00 2001 From: Bruce Ashton Date: Mon, 28 Sep 2026 22:27:15 +1300 Subject: [PATCH 5/5] Android: bump app version to 3.6.7-2 * Set versionName to 3.6.7-2 and versionCode to 3672. * Bump assets/ver to 107 so ForkFront copies the game data again on update. The data files now depend on the ABI's word size, so a device upgrading from a 32-bit 3.6.7-1 install to a 64-bit build must not keep the old files. Only the last digit changes, which keeps the user's defaults file and does not delete save or bones files: those are still valid on devices staying on a 32-bit build, and NetHack rejects incompatible ones itself with a "Configuration incompatibility" message. Co-Authored-By: Claude Opus 5.5 --- sys/android/app/assets/ver | 2 +- sys/android/app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sys/android/app/assets/ver b/sys/android/app/assets/ver index fe4afb0df8..e34885bbc6 100644 --- a/sys/android/app/assets/ver +++ b/sys/android/app/assets/ver @@ -1 +1 @@ -106 +107 diff --git a/sys/android/app/build.gradle b/sys/android/app/build.gradle index 815c6058c7..739df55dba 100644 --- a/sys/android/app/build.gradle +++ b/sys/android/app/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { minSdkVersion 7 targetSdkVersion 30 - versionName '3.6.7-1' - versionCode 3671 + versionName '3.6.7-2' + versionCode 3672 } compileOptions {