Skip to content

fix(android): resolve black screen, modernize build with vcpkg and LuaJIT#1673

Open
Kizuno18 wants to merge 1 commit intoopentibiabr:mainfrom
Kizuno18:fix/android-black-screen
Open

fix(android): resolve black screen, modernize build with vcpkg and LuaJIT#1673
Kizuno18 wants to merge 1 commit intoopentibiabr:mainfrom
Kizuno18:fix/android-black-screen

Conversation

@Kizuno18
Copy link
Copy Markdown
Contributor

@Kizuno18 Kizuno18 commented Mar 26, 2026

Summary

Fixes the Android black screen issue (#1642) reported by @tomasz1337 and confirmed by @maianame. Modernizes the Android build pipeline by replacing the pre-compiled Google Drive library archive with a self-contained build system.

Fixes #1642 — Android Black Screen

The black screen was caused by multiple cascading issues:

  1. Missing data.zip null checkunZipAssetData() dereferences null AAsset* when data.zip is missing from APK assets, crashing silently
  2. unzipper.cpp nested directory failurecreate_directory() changed to create_directories() + parent dir creation before file writes
  3. APP_CMD_LOW_MEMORY destroying EGL surface permanently — system never re-sends APP_CMD_INIT_WINDOW after low-memory, app enters permanent blocking wait with no recovery path
  4. Keyboard auto-opening on programmatic focus — only open on Fw::MouseFocusReason (user tap), not on code-driven focus() calls (e.g. entergame.lua:582)

Build modernization

Replaces the pre-compiled 2022 Google Drive archive with a self-contained build:

  • LuaJIT: Cross-compiled from source via build_luajit_android.sh for all 4 ABIs using the official cross-compilation approach
  • Other deps: via vcpkg (OpenAL, Freetype, protobuf, etc.)
  • minizip: Vendored as source (nmoinvaz fork) in src/framework/core/minizip/
  • GLES3: Separated Android (ES3) from WASM (ES2)

LuaJIT cross-compilation

The vcpkg LuaJIT port cannot cross-compile for Android from an x64 host (buildvm-32 feature fails with "Unsupported target architecture"). We solved this by cross-compiling LuaJIT separately using the official Makefile approach with specific flags discovered through debugging:

  • 32-bit pointer size mismatch (LuaJIT #664): For armeabi-v7a and x86, HOST_CC="gcc -m32" is mandatory (gcc-multilib required). Without it, buildvm computes 64-bit struct offsets for a 32-bit target, causing silent Lua stack corruption that manifests as hasIndex(-n) assertion failures at runtime.
  • -DLUAJIT_UNWIND_EXTERNAL: Required for Android NDK because clang generates unwind tables (-funwind-tables). Without this flag, LuaJIT uses its internal frame unwinder which conflicts with C++ exception handling in the NDK runtime, corrupting the Lua stack when C++ code interacts with LuaJIT across exception boundaries. The Makefile auto-detects this but can silently fail if cross-compiler paths are misconfigured.
  • Never TARGET_SYS=Android (LuaJIT #440): causes memory corruption in lua_openlibs. Use TARGET_SYS=Linux or omit (auto-detected on Linux hosts).
  • amalg target: Single-file build for better optimization, matching the vcpkg approach.

Full build guide with troubleshooting in android/BUILDING.md. References: LuaJIT #477, #664, #440, vcpkg portfile.

ALOGD debug logging

Added __android_log_print calls throughout main.cpp (platform init, unzip, resource loading, work dir discovery, framework init, client init, init.lua execution). These are ANDROID_LOG_DEBUG level — only visible via adb logcat -s "OTClientMobile:*" and have no performance impact on release builds. This was the key missing piece for debugging #1642: without these logs, there was no way to determine where the initialization was failing.

Multi-ABI support

ABI LuaJIT host flag Status
arm64-v8a gcc Tested on physical device (Samsung, Mali-G52 MC2, Android 12)
armeabi-v7a gcc -m32 Compiled and linked
x86_64 gcc Compiled and linked
x86 gcc -m32 Compiled and linked

Build instructions

See android/BUILDING.md. Quick start:

git clone https://github.com/LuaJIT/LuaJIT.git luajit-src
cd luajit-src && git checkout d0e88930 && cd ..
./setup_android_deps.sh
cd android && ./gradlew assembleRelease

Test plan

  • Debug + signed release APK (WSL Ubuntu 24.04, Java 17, NDK 29, Gradle 8.14.2)
  • Physical device (Samsung, Mali-G52 MC2, Android 12, arm64-v8a)
  • All ~60 Lua modules load, login screen renders correctly
  • Keyboard only opens on user tap
  • No crash on APP_CMD_LOW_MEMORY
  • EGL context recovery on minimize/restore — verified working
  • LuaJIT cross-compiled for all 4 ABIs

Summary by CodeRabbit

  • New Features

    • Android APK build support, Docker-based Android builder, and helper scripts to prepare per-ABI native artifacts.
  • Documentation

    • Added a comprehensive Android BUILDING guide covering native builds, Docker and WSL workflows.
  • Bug Fixes

    • Safer ZIP extraction (zip-slip checks) and clearer handling when bundled asset data is missing.
  • Other

    • Enhanced ZIP/unzip and in-memory ZIP support, Android manifest now requires OpenGL ES 3.0, updated ignore files.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Warning

Rate limit exceeded

@Kizuno18 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 15 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8921e6d-7f3e-4a3d-9e0f-1327c5f04e5e

📥 Commits

Reviewing files that changed from the base of the PR and between dc7c9fb and 29c1e12.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end Android build support: new Docker Android builder, SDK/NDK/CMake/vcpkg provisioning, LuaJIT cross-compilation and helper scripts, Gradle/CMake/manifest adjustments, a vendored minizip unzip stack, Android runtime fixes, and build documentation.

Changes

Cohort / File(s) Summary
Ignore patterns
\.dockerignore, \.gitignore
Broadened Docker ignore globs (Dockerfile*, build dirs, compiled artifacts) and added Android/Gradle/LuaJIT entries to gitignore.
Docker build
Dockerfile.android
New multi-stage Dockerfile provisioning SDK/NDK/CMake, bootstrapping vcpkg, cross-compiling LuaJIT, creating data.zip, and running Gradle to produce APK artifacts.
Docs
android/BUILDING.md
New Android BUILDING guide with prerequisites, environment vars, quick and Docker build flows, LuaJIT cross-compile steps, ABI mapping, and troubleshooting.
Helper scripts
build_luajit_android.sh, setup_android_deps.sh, setup_android_wsl.sh
New executables to cross-compile LuaJIT per-ABI, install vcpkg triplets and dependencies, produce data.zip, and automate WSL SDK/NDK setup and Gradle builds.
Android Gradle & manifest
android/app/build.gradle.kts, android/app/src/main/AndroidManifest.xml
Adjusted ABI ordering, removed -flto, added vcpkg manifest/install flags, signing config for release (debug keystore), updated CMake/NDK versions, and raised GLES requirement to 3.0.
CMake / build integration
src/CMakeLists.txt
Android: set OPENGL_ES=3, require ABI-specific prebuilt LuaJIT libs under android/app/libs, vendor minizip sources, use find_package(OpenAL), and add Freetype::Freetype to links.
Minizip stack (new)
src/framework/core/minizip/...
crypt.c, crypt.h, ioapi.c, ioapi.h, ioapi_mem.c, ioapi_mem.h, unzip.c, unzip.h
Adds full unzip implementation: PKWARE legacy crypto, POSIX/Zip64 IO bridge, memory-backed IO, and a Zip64-aware unarchiver with public headers and APIs.
Unzip integration & extraction
src/framework/core/unzipper.cpp, src/framework/platform/androidmanager.cpp
Switched includes to new minizip headers, create directories recursively, added zip-slip path normalization/skip checks, and early-check for missing data.zip asset.
Android runtime & UI
src/framework/platform/androidwindow.cpp, src/main.cpp, src/framework/ui/uitextedit.cpp
Added Android logcat debug traces, GLES3-aware EGL selection/context, improved surface recovery, adjusted touch-to-mouse mapping and drag thresholds, and restricted soft-key behavior for text focus.

Sequence Diagram

sequenceDiagram
    actor Developer
    participant DockerCLI as "Docker CLI"
    participant Builder as "Builder Stage"
    participant SDKNDK as "Android SDK/NDK/CMake"
    participant VCPKG as "vcpkg"
    participant Gradle as "Gradle"
    participant APK as "APK Artifact"

    Developer->>DockerCLI: docker build -f Dockerfile.android
    DockerCLI->>Builder: create builder container
    Builder->>SDKNDK: sdkmanager install SDK/NDK/CMake
    Builder->>VCPKG: clone & bootstrap vcpkg
    Builder->>VCPKG: vcpkg install triplets
    Builder->>SDKNDK: cross-compile LuaJIT per ABI (build_luajit_android.sh)
    Builder->>Builder: assemble assets -> android/app/src/main/assets/data.zip
    Builder->>Gradle: ./gradlew assembleRelease
    Gradle->>SDKNDK: externalNativeBuild (CMake uses vcpkg & prebuilt LuaJIT)
    Gradle->>APK: package and sign release APK
    APK->>Developer: artifact available from builder stage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • majestyotbr
  • andreoam

Poem

🐰 I hopped through SDKs, NDK and zip,
Built JIT for ABIs with a skip and a nip,
Docker warmed the builder bright,
Assets zipped, Gradle sealed it tight—
A rabbit’s hop to Android night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: fixing Android black screen issues and modernizing the build system with vcpkg and LuaJIT cross-compilation.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #1642: null check for AAssetManager_open [androidmanager.cpp], nested directory creation fixes [unzipper.cpp], EGL surface recovery [androidwindow.cpp], keyboard focus restriction [uitextedit.cpp], LuaJIT cross-compilation scripts [build_luajit_android.sh, setup_android_deps.sh], vcpkg dependency management, minizip vendoring, GLES3 separation, and debug logging [main.cpp, androidwindow.cpp].
Out of Scope Changes check ✅ Passed All changes are directly related to Android build modernization and black-screen fixes. Minor additions like .dockerignore, .gitignore, Dockerfile.android, and android/BUILDING.md support the core objective of providing a reproducible Android build system with proper documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/framework/platform/androidmanager.cpp (1)

113-120: ⚠️ Potential issue | 🔴 Critical

Undefined behavior: delete[] used on malloc-allocated memory.

Line 113 allocates with malloc() but line 120 deallocates with delete[]. Mixing C and C++ allocation/deallocation functions is undefined behavior and can cause crashes or memory corruption.

🐛 Proposed fix
     auto dataFileLength = AAsset_getLength(dataAsset);
     char* dataContent = (char *) malloc(dataFileLength + 1);
+    if (!dataContent) {
+        AAsset_close(dataAsset);
+        g_logger.fatal("Failed to allocate memory for data.zip extraction");
+        return;
+    }
     AAsset_read(dataAsset, dataContent, dataFileLength);
     dataContent[dataFileLength] = '\0';
 
     unzipper::extract(dataContent, dataFileLength, destFolder);
 
     AAsset_close(dataAsset);
-    delete [] dataContent;
+    free(dataContent);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/platform/androidmanager.cpp` around lines 113 - 120, The code
allocates dataContent with malloc but deallocates it with delete[], causing
undefined behavior; replace the deallocation with free(dataContent) (or
alternatively allocate with new char[...] and keep delete[]) and ensure
<cstdlib> is included if free isn't already available; verify the buffer passed
to unzipper::extract still uses dataContent and AAsset_close remains after free.
src/framework/core/unzipper.cpp (2)

47-48: ⚠️ Potential issue | 🟠 Major

Memory leak: unzmem.base is never freed.

The buffer allocated with malloc at line 47 is never freed after unzClose(). Add cleanup before returning.

🛠️ Proposed fix
     unzClose(zipfile);
+    free(unzmem.base);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/unzipper.cpp` around lines 47 - 48, The malloc'd buffer
assigned to unzmem.base is never freed; ensure you free(unzmem.base) after
you're done with the unzipper (e.g., immediately after calling unzClose()) and
also on all early-return/error paths in the function to avoid leaks—update the
code paths around unzmem.base allocation and the unzClose() call so that
unzmem.base is freed exactly once when the in-memory unz structure is no longer
needed.

120-120: ⚠️ Potential issue | 🔴 Critical

Invalid error message construction will cause undefined behavior.

The expression &"error: " [ error] performs pointer arithmetic on a string literal using the error code as an index, which produces garbage output or crashes. This should format the error code properly.

🐛 Proposed fix
-                    g_logger.fatal( &"error: " [ error] );
+                    g_logger.fatal(stdext::format("unzip read error: %d", error));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/unzipper.cpp` at line 120, The call to g_logger.fatal
currently uses the invalid expression &"error: " [ error], which performs
unintended pointer arithmetic; fix by building a proper formatted message that
includes the numeric/enum error value and passing that to g_logger.fatal (e.g.
use the logger's printf-style overload or construct a std::string via
std::to_string/error-to-string conversion or an ostringstream and pass that),
replacing the faulty expression where g_logger.fatal(...) is called and using
the local variable error as the value to include.
src/framework/platform/androidwindow.cpp (1)

270-280: ⚠️ Potential issue | 🟡 Minor

Inconsistent clock sources for long-press detection.

Line 271 records m_lastPress using g_clock.millis(), but line 274 compares against stdext::millis(). If these clock sources have different bases or drift, long-press detection could behave unexpectedly. Consider using the same clock source for both.

Proposed fix
-        if (!m_isDragging && stdext::millis() > m_lastPress + 500) {
+        if (!m_isDragging && g_clock.millis() > m_lastPress + 500) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/platform/androidwindow.cpp` around lines 270 - 280, The
long-press detection mixes clock sources: m_lastPress is set from
g_clock.millis() but later compared to stdext::millis(); update the comparison
to use the same clock (e.g., replace stdext::millis() with g_clock.millis() or
vice versa) so the check in the TOUCH_UP branch (the if that uses m_lastPress +
500) uses the same clock that set m_lastPress; adjust references around
m_lastPress, g_clock.millis(), stdext::millis() in the TOUCH_UP handling
(including the block that sets mouseButton to Fw::MouseRightButton and the
subsequent g_dispatcher.addEvent that modifies m_mouseButtonStates) so timing is
consistent.
🧹 Nitpick comments (9)
build_luajit_android.sh (2)

43-43: Quote $(nproc) to prevent word splitting.

Shellcheck (SC2046) flags the unquoted command substitution. While unlikely to cause issues with nproc, quoting is the safe practice.

🔧 Proposed fix
-    make -j$(nproc) amalg \
+    make -j"$(nproc)" amalg \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build_luajit_android.sh` at line 43, The make invocation uses an unquoted
command substitution in the parallel flag (make -j$(nproc) ...), which can
trigger word-splitting warnings; update the make command to quote the
substitution so it reads make -j"$(nproc)" (i.e., replace $(nproc) with
"$(nproc)" in the make -j usage) to satisfy ShellCheck SC2046 and avoid
potential splitting issues.

24-27: apt-get assumes Debian/Ubuntu and may fail silently on other distributions.

The automatic installation of gcc-multilib only works on Debian-based systems. Consider adding a more informative error message for non-Debian environments or documenting this requirement.

🔧 Proposed improvement
 # Verify gcc -m32 works (needed for 32-bit targets)
 if ! gcc -m32 -x c -c /dev/null -o /dev/null 2>/dev/null; then
-    echo "Installing gcc-multilib for 32-bit cross-compilation..."
-    sudo apt-get install -y gcc-multilib g++-multilib
+    echo "32-bit compilation support (gcc -m32) not available."
+    if command -v apt-get &>/dev/null; then
+        echo "Installing gcc-multilib for 32-bit cross-compilation..."
+        sudo apt-get install -y gcc-multilib g++-multilib
+    else
+        echo "ERROR: Please install 32-bit GCC support manually (e.g., gcc-multilib on Debian/Ubuntu)"
+        exit 1
+    fi
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build_luajit_android.sh` around lines 24 - 27, The check that installs
gcc-multilib assumes a Debian-based system; update the if-block that tests "gcc
-m32 ..." to first detect the package manager (e.g., check for apt-get) and only
attempt "sudo apt-get install -y gcc-multilib g++-multilib" when apt-get is
present, otherwise print a clear, actionable error message describing the
requirement and supported package managers (or instruct the user to install
equivalent packages for their distro) and exit non-zero; reference the
installation block that runs "sudo apt-get install -y gcc-multilib g++-multilib"
so you modify that branch to perform package-manager detection and user-friendly
messaging.
android/app/build.gradle.kts (1)

36-43: Misleading signing configuration name.

The "release" signing config uses the debug keystore, which will produce APKs that cannot be uploaded to Play Store. Consider renaming to "debugRelease" or adding a comment clarifying this is for testing only.

📝 Proposed clarification
     signingConfigs {
-        create("release") {
+        // Debug-signed release for testing; replace with production keystore for Play Store
+        create("release") {
             storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/build.gradle.kts` around lines 36 - 43, The signingConfigs block
defines create("release") but points to the debug keystore, which is misleading;
update the config name from "release" to something explicit like "debugRelease"
(or add a clear inline comment) and keep the existing values (storeFile =
file(System.getProperty("user.home") + "/.android/debug.keystore"),
storePassword/keyAlias/keyPassword) so callers don’t mistake this for a real
Play Store release signing config; ensure any references to the signing config
elsewhere (e.g., in buildTypes or signingConfig usage) are updated to the new
name.
Dockerfile.android (2)

4-4: Container runs as root user.

Static analysis flags that the last USER is root (DS-0002). This is acceptable for a build-only container that outputs artifacts to a scratch stage, but consider documenting this decision or adding a non-root user if the image may be used interactively.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.android` at line 4, The Dockerfile ends with USER root which
triggers a static-analysis warning; either explicitly document that this is a
build-only stage and safe to run as root (add a comment above the USER root line
explaining artifacts are copied to a scratch/producer stage), or change to a
non-root runtime user by adding commands to create and set a less-privileged
user (e.g., RUN groupadd -r app && useradd -r -g app app && chown -R app:app
/output && then replace USER root with USER app) so the image does not end
running as root.

13-14: Pin vcpkg to a specific commit for reproducible builds.

The unpinned vcpkg clone may pull incompatible changes. Use the same commit as builtin-baseline in vcpkg.json.

♻️ Proposed fix
-RUN git clone https://github.com/microsoft/vcpkg.git \
-    && cd vcpkg && ./bootstrap-vcpkg.sh
+RUN git clone https://github.com/microsoft/vcpkg.git \
+    && cd vcpkg && git checkout b322364f06308bdd24823f9d8f03fe0cc86fd46f \
+    && ./bootstrap-vcpkg.sh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.android` around lines 13 - 14, The Dockerfile.android currently
clones vcpkg without pinning, causing non-reproducible builds; update the RUN
that performs "git clone ... vcpkg" / "cd vcpkg" / "./bootstrap-vcpkg.sh" to
checkout the specific commit referenced by the vcpkg.json builtin-baseline
before running bootstrap; find the commit SHA in vcpkg.json and ensure the
Dockerfile checks out that SHA (e.g., after cloning and before bootstrapping) so
builds are reproducible.
setup_android_deps.sh (2)

103-106: Using sudo in build script may be unexpected.

Auto-installing packages with sudo apt-get can surprise users who expect build scripts to be non-privileged. Consider adding a prompt or documenting this behavior.

📝 Proposed improvement
 if ! gcc -m32 -x c -c /dev/null -o /dev/null 2>/dev/null; then
-    echo "Installing gcc-multilib for 32-bit cross-compilation..."
-    sudo apt-get install -y gcc-multilib g++-multilib
+    echo "WARNING: gcc-multilib required for 32-bit builds but not found."
+    echo "Installing with: sudo apt-get install -y gcc-multilib g++-multilib"
+    read -p "Continue? [y/N] " -n 1 -r
+    echo
+    if [[ $REPLY =~ ^[Yy]$ ]]; then
+        sudo apt-get install -y gcc-multilib g++-multilib
+    else
+        echo "Skipping 32-bit ABIs (armeabi-v7a, x86)"
+        # Filter out 32-bit ABIs from ABI_MAP
+        unset 'ABI_MAP[armeabi-v7a]' 'ABI_MAP[x86]'
+    fi
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` around lines 103 - 106, The block that auto-installs
multilib using "sudo apt-get install -y gcc-multilib g++-multilib" should not
run without explicit user consent; replace the automatic sudo install in the
if-check (the snippet starting with "if ! gcc -m32 -x c -c /dev/null -o
/dev/null") with a non-privileged prompt or clear message: detect missing 32-bit
compiler as now, then print instructions for the user to run the install command
themselves (or ask for confirmation before calling sudo), so the script no
longer surprises users by invoking sudo without explicit approval.

55-55: Quote $(nproc) to satisfy shellcheck.

While safe in this context, quoting prevents potential issues if nproc returns unexpected output.

♻️ Proposed fix
-    make -j$(nproc) \
+    make -j"$(nproc)" \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` at line 55, Quote the subshell expansion used for
make's job count to satisfy shellcheck: in the line containing the make
invocation with the -j$(nproc) argument, surround the $(nproc) substitution with
double quotes so the -j value is provided as "-$(nproc)" (i.e., change
-j$(nproc) to use a quoted subshell expansion) to avoid potential issues if
nproc outputs unexpected whitespace or characters.
setup_android_wsl.sh (1)

4-6: Hardcoded paths reduce script portability.

The paths /home/dev/... assume a specific environment. Consider using environment variables with defaults or deriving paths from the script location.

♻️ Proposed fix
-ANDROID_HOME=/home/dev/android-sdk
-VCPKG_ROOT=/home/dev/vcpkg
-PROJECT_DIR=/home/dev/kizu-otc
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
+VCPKG_ROOT="${VCPKG_ROOT:-$HOME/vcpkg}"
+PROJECT_DIR="${PROJECT_DIR:-$(dirname "$SCRIPT_DIR")}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_wsl.sh` around lines 4 - 6, Replace the hardcoded paths set in
ANDROID_HOME, VCPKG_ROOT, and PROJECT_DIR with values derived from environment
variables and sensible defaults or from the script location; for example, read
existing env vars (ANDROID_HOME, VCPKG_ROOT, PROJECT_DIR) and fall back to
"$HOME/android-sdk" or a path relative to the script directory (use dirname
"$0") if not set, so the variables in the script are assigned via
environment-aware defaults rather than fixed /home/dev/... literals.
src/framework/core/minizip/ioapi.c (1)

99-110: Memory leak and potential null pointer dereference in file_build_ioposix.

This vendored code has potential issues:

  1. Line 104-108: No null checks after malloc() calls - could dereference null on allocation failure
  2. Line 109: If malloc for ioposix->filename (line 107) fails, the previously allocated ioposix is leaked

Since this is vendored code from the established minizip library, these are pre-existing issues. Consider whether to patch locally or accept as-is given the low likelihood of malloc failure in practice.

Optional defensive fix
 static voidpf file_build_ioposix(FILE *file, const char *filename)
 {
     FILE_IOPOSIX *ioposix = NULL;
     if (file == NULL)
         return NULL;
     ioposix = (FILE_IOPOSIX*)malloc(sizeof(FILE_IOPOSIX));
+    if (ioposix == NULL)
+        return NULL;
     ioposix->file = file;
     ioposix->filenameLength = (int)strlen(filename) + 1;
     ioposix->filename = (char*)malloc(ioposix->filenameLength * sizeof(char));
+    if (ioposix->filename == NULL) {
+        free(ioposix);
+        return NULL;
+    }
     strncpy((char*)ioposix->filename, filename, ioposix->filenameLength);
     return (voidpf)ioposix;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/ioapi.c` around lines 99 - 110, In
file_build_ioposix, add defensive null checks after each malloc: check the
allocation of ioposix (from malloc(sizeof(FILE_IOPOSIX))) and return NULL on
failure; then allocate ioposix->filename and if that malloc fails free(ioposix)
and return NULL to avoid leaking the FILE_IOPOSIX struct; only assign
ioposix->filenameLength and copy the filename after successful allocation (and
handle a NULL filename parameter if needed) so you never dereference null
pointers or leak memory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/BUILDING.md`:
- Around line 99-102: The docs show running the otclient-android image to
extract the APK but the Dockerfile.android uses a scratch final stage so docker
run can't produce files; update the instructions to build the image
(Dockerfile.android -> tag otclient-android) and then extract the artifact by
creating a container from otclient-android and using docker cp to copy
/app-release.apk (or the correct artifact path) to the host, then remove the
temporary container, or alternatively recommend using docker build --output to
directly emit the apk; reference the otclient-android image name and the scratch
final stage when making the change.

In `@Dockerfile.android`:
- Line 2: Update the Dockerfile's base image FROM instruction (the "FROM
thyrlian/android-sdk:latest AS builder" line) to pin a specific, stable tag or
digest instead of :latest; replace ":latest" with a chosen version string or
immutable digest (e.g., a semantic version or SHA256 digest) so builds are
reproducible and won't break when the upstream image changes.

In `@setup_android_deps.sh`:
- Around line 72-74: The headers are being installed to
"$INSTALL_DIR/include/luajit-2.1" but src/CMakeLists.txt expects
"include/luajit/", so change the install path in this script to match
CMakeLists: update the mkdir target and the cp destination from
include/luajit-2.1 to include/luajit (i.e., replace mkdir -p
"$INSTALL_DIR/include/luajit-2.1" and the cp target
"$INSTALL_DIR/include/luajit-2.1/" so headers land in
"$INSTALL_DIR/include/luajit/"); alternatively, if you prefer to change CMake,
update the include directory reference in src/CMakeLists.txt to
"include/luajit-2.1"—pick one consistent path and apply it in both the mkdir/cp
lines and the CMakeLists.txt include reference.

In `@src/CMakeLists.txt`:
- Around line 187-200: The LuaJIT include directory constant LUAJIT_ANDROID_INC
points to "include/luajit" but the setup script installs headers to
"include/luajit-2.1", causing include path mismatch; update the CMake variable
LUAJIT_ANDROID_INC (and any uses: LUAJIT_INCLUDE_DIR, include_directories) to
"${LUAJIT_ANDROID_LIBS}/include/luajit-2.1" (or alternatively change the
setup_android_deps.sh install path to "include/luajit") so the include path used
by set(LUAJIT_ANDROID_INC ...), set(LUAJIT_INCLUDE_DIR ...), and
include_directories(...) matches the actual installed headers.

In `@src/framework/core/minizip/crypt.h`:
- Around line 19-28: The header is missing the <stdint.h> include required for
fixed-width types used in this file (uint8_t, uint32_t, int32_t); add a single
`#include` <stdint.h> near the top of the file (alongside existing includes and
before any function declarations) so symbols used in prototypes (e.g., uses of
uint8_t/uint32_t/int32_t in the minizip crypt API) are defined; keep it inside
the include-guard (_MINICRYPT_H) and after any C++ extern "C" guard setup as
appropriate.

In `@src/framework/core/minizip/ioapi_mem.c`:
- Around line 91-100: The code grows the in-memory buffer by allocating newbase
and immediately uses memcpy without checking malloc result; add a NULL check
after newbase = (char *)malloc(newmemsize) in the function that manipulates mem
(uses mem->base, mem->size and IOMEM_BUFFERSIZE) and handle allocation failure
by leaving mem unchanged and returning an error/false (or appropriate failure
code for the surrounding API) instead of calling memcpy/free on NULL; only
proceed to memcpy(mem->base, mem->size) and update mem->base/mem->size when
newbase != NULL.

In `@src/framework/core/minizip/unzip.c`:
- Around line 1440-1447: The calculation of total_out_before/total_out_after in
unzip.c uses (((uint32_t)s->pfile_in_zip_read->bstream.total_out_hi32) << 32)
which performs a 32-bit shift on a 32-bit type (undefined behavior); cast
total_out_hi32 to a 64-bit type before shifting (e.g.,
(uint64_t)s->pfile_in_zip_read->bstream.total_out_hi32 << 32) and ensure the sum
is computed in 64-bit (uint64_t) to produce correct 64-bit totals around the
BZ2_bzDecompress call and its surrounding uses (refer to total_out_before,
total_out_after, pfile_in_zip_read->bstream.total_out_hi32 and
BZ2_bzDecompress).

In `@src/framework/platform/androidwindow.cpp`:
- Around line 338-344: The AndroidWindow methods call
g_androidManager.getClipboardText() and g_androidManager.setClipboardText(...),
but AndroidManager lacks those declarations; add corresponding public member
function declarations and implementations to AndroidManager (e.g., std::string
getClipboardText() and void setClipboardText(const std::string&)) that forward
to the platform clipboard logic used elsewhere, and ensure their signatures
match how AndroidWindow invokes them (returning std::string and accepting a
const std::string& or std::string parameter); update AndroidManager header and
implementation files accordingly so g_androidManager.getClipboardText() and
g_androidManager.setClipboardText(...) compile.

---

Outside diff comments:
In `@src/framework/core/unzipper.cpp`:
- Around line 47-48: The malloc'd buffer assigned to unzmem.base is never freed;
ensure you free(unzmem.base) after you're done with the unzipper (e.g.,
immediately after calling unzClose()) and also on all early-return/error paths
in the function to avoid leaks—update the code paths around unzmem.base
allocation and the unzClose() call so that unzmem.base is freed exactly once
when the in-memory unz structure is no longer needed.
- Line 120: The call to g_logger.fatal currently uses the invalid expression
&"error: " [ error], which performs unintended pointer arithmetic; fix by
building a proper formatted message that includes the numeric/enum error value
and passing that to g_logger.fatal (e.g. use the logger's printf-style overload
or construct a std::string via std::to_string/error-to-string conversion or an
ostringstream and pass that), replacing the faulty expression where
g_logger.fatal(...) is called and using the local variable error as the value to
include.

In `@src/framework/platform/androidmanager.cpp`:
- Around line 113-120: The code allocates dataContent with malloc but
deallocates it with delete[], causing undefined behavior; replace the
deallocation with free(dataContent) (or alternatively allocate with new
char[...] and keep delete[]) and ensure <cstdlib> is included if free isn't
already available; verify the buffer passed to unzipper::extract still uses
dataContent and AAsset_close remains after free.

In `@src/framework/platform/androidwindow.cpp`:
- Around line 270-280: The long-press detection mixes clock sources: m_lastPress
is set from g_clock.millis() but later compared to stdext::millis(); update the
comparison to use the same clock (e.g., replace stdext::millis() with
g_clock.millis() or vice versa) so the check in the TOUCH_UP branch (the if that
uses m_lastPress + 500) uses the same clock that set m_lastPress; adjust
references around m_lastPress, g_clock.millis(), stdext::millis() in the
TOUCH_UP handling (including the block that sets mouseButton to
Fw::MouseRightButton and the subsequent g_dispatcher.addEvent that modifies
m_mouseButtonStates) so timing is consistent.

---

Nitpick comments:
In `@android/app/build.gradle.kts`:
- Around line 36-43: The signingConfigs block defines create("release") but
points to the debug keystore, which is misleading; update the config name from
"release" to something explicit like "debugRelease" (or add a clear inline
comment) and keep the existing values (storeFile =
file(System.getProperty("user.home") + "/.android/debug.keystore"),
storePassword/keyAlias/keyPassword) so callers don’t mistake this for a real
Play Store release signing config; ensure any references to the signing config
elsewhere (e.g., in buildTypes or signingConfig usage) are updated to the new
name.

In `@build_luajit_android.sh`:
- Line 43: The make invocation uses an unquoted command substitution in the
parallel flag (make -j$(nproc) ...), which can trigger word-splitting warnings;
update the make command to quote the substitution so it reads make -j"$(nproc)"
(i.e., replace $(nproc) with "$(nproc)" in the make -j usage) to satisfy
ShellCheck SC2046 and avoid potential splitting issues.
- Around line 24-27: The check that installs gcc-multilib assumes a Debian-based
system; update the if-block that tests "gcc -m32 ..." to first detect the
package manager (e.g., check for apt-get) and only attempt "sudo apt-get install
-y gcc-multilib g++-multilib" when apt-get is present, otherwise print a clear,
actionable error message describing the requirement and supported package
managers (or instruct the user to install equivalent packages for their distro)
and exit non-zero; reference the installation block that runs "sudo apt-get
install -y gcc-multilib g++-multilib" so you modify that branch to perform
package-manager detection and user-friendly messaging.

In `@Dockerfile.android`:
- Line 4: The Dockerfile ends with USER root which triggers a static-analysis
warning; either explicitly document that this is a build-only stage and safe to
run as root (add a comment above the USER root line explaining artifacts are
copied to a scratch/producer stage), or change to a non-root runtime user by
adding commands to create and set a less-privileged user (e.g., RUN groupadd -r
app && useradd -r -g app app && chown -R app:app /output && then replace USER
root with USER app) so the image does not end running as root.
- Around line 13-14: The Dockerfile.android currently clones vcpkg without
pinning, causing non-reproducible builds; update the RUN that performs "git
clone ... vcpkg" / "cd vcpkg" / "./bootstrap-vcpkg.sh" to checkout the specific
commit referenced by the vcpkg.json builtin-baseline before running bootstrap;
find the commit SHA in vcpkg.json and ensure the Dockerfile checks out that SHA
(e.g., after cloning and before bootstrapping) so builds are reproducible.

In `@setup_android_deps.sh`:
- Around line 103-106: The block that auto-installs multilib using "sudo apt-get
install -y gcc-multilib g++-multilib" should not run without explicit user
consent; replace the automatic sudo install in the if-check (the snippet
starting with "if ! gcc -m32 -x c -c /dev/null -o /dev/null") with a
non-privileged prompt or clear message: detect missing 32-bit compiler as now,
then print instructions for the user to run the install command themselves (or
ask for confirmation before calling sudo), so the script no longer surprises
users by invoking sudo without explicit approval.
- Line 55: Quote the subshell expansion used for make's job count to satisfy
shellcheck: in the line containing the make invocation with the -j$(nproc)
argument, surround the $(nproc) substitution with double quotes so the -j value
is provided as "-$(nproc)" (i.e., change -j$(nproc) to use a quoted subshell
expansion) to avoid potential issues if nproc outputs unexpected whitespace or
characters.

In `@setup_android_wsl.sh`:
- Around line 4-6: Replace the hardcoded paths set in ANDROID_HOME, VCPKG_ROOT,
and PROJECT_DIR with values derived from environment variables and sensible
defaults or from the script location; for example, read existing env vars
(ANDROID_HOME, VCPKG_ROOT, PROJECT_DIR) and fall back to "$HOME/android-sdk" or
a path relative to the script directory (use dirname "$0") if not set, so the
variables in the script are assigned via environment-aware defaults rather than
fixed /home/dev/... literals.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 99-110: In file_build_ioposix, add defensive null checks after
each malloc: check the allocation of ioposix (from malloc(sizeof(FILE_IOPOSIX)))
and return NULL on failure; then allocate ioposix->filename and if that malloc
fails free(ioposix) and return NULL to avoid leaking the FILE_IOPOSIX struct;
only assign ioposix->filenameLength and copy the filename after successful
allocation (and handle a NULL filename parameter if needed) so you never
dereference null pointers or leak memory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52360faf-768b-4c9c-abde-41ee37b1eb23

📥 Commits

Reviewing files that changed from the base of the PR and between 294896c and 9d5dfa4.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp

@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch from 9d5dfa4 to fb15559 Compare March 26, 2026 06:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/framework/core/unzipper.cpp (1)

86-103: ⚠️ Potential issue | 🟠 Major

Block zip-slip paths before writing extracted files.

filename is still appended to destinationPath verbatim. With the new create_directories() calls, entries like ../... or absolute paths can now escape the extraction root instead of failing implicitly.

🔒 Suggested guard
+        const auto entryPath = std::filesystem::path(filename).lexically_normal();
+        if (entryPath.is_absolute() || entryPath.string().starts_with("..")) {
+            unzClose(zipfile);
+            g_logger.fatal("zip entry escapes destination folder");
+        }
+        const auto outputPath = destinationFolder / entryPath;
+
         if (filename[ filename_length-1 ] == dir_delimiter )
         {
-            std::filesystem::create_directories({ destinationPath + filename });
+            std::filesystem::create_directories(outputPath);
         }
         else
         {
             // Entry is a file, so extract it.
             if ( unzOpenCurrentFile( zipfile ) != UNZ_OK )
@@
-            std::string destFilePath = destinationPath + filename;
+            const std::string destFilePath = outputPath.string();
             // Ensure parent directory exists (zip may not have explicit directory entries)
-            std::filesystem::path parentDir = std::filesystem::path(destFilePath).parent_path();
+            const std::filesystem::path parentDir = outputPath.parent_path();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/unzipper.cpp` around lines 86 - 103, Reject or sanitize
zip entries that try to escape the intended extraction root: build the
extraction target using std::filesystem::path dest =
std::filesystem::path(destinationPath) / filename and normalize it (e.g., dest =
dest.lexically_normal()), compute the normalized extraction root
std::filesystem::path root =
std::filesystem::path(destinationPath).lexically_normal(), then verify dest is
not absolute and that dest.string() begins with root.string() (or use
std::mismatch/compare on path components) — if the check fails, log an error and
skip the entry instead of creating directories or writing the file; apply this
check before using destFilePath/destFilePath.parent_path(), before calling
create_directories(), and before opening the file after unzOpenCurrentFile in
unzipper.cpp.
🧹 Nitpick comments (3)
src/CMakeLists.txt (1)

241-244: Scope the vendored unzip sources to Android only.

framework/core/unzipper.cpp is already Android-only, so compiling the four MiniZip C files into every desktop/WASM build just adds dead code and extra portability surface.

♻️ Suggested scoping
-        framework/core/minizip/ioapi.c
-        framework/core/minizip/ioapi_mem.c
-        framework/core/minizip/unzip.c
-        framework/core/minizip/crypt.c
if(ANDROID)
  list(APPEND SOURCE_FILES
    framework/core/minizip/ioapi.c
    framework/core/minizip/ioapi_mem.c
    framework/core/minizip/unzip.c
    framework/core/minizip/crypt.c
  )
endif()

Move the matching set_source_files_properties(...) block under the same if(ANDROID) guard as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CMakeLists.txt` around lines 241 - 244, The four MiniZip C sources
(framework/core/minizip/ioapi.c, ioapi_mem.c, unzip.c, crypt.c) are being added
unconditionally; wrap the list(APPEND SOURCE_FILES ... ) that includes those
filenames in an if(ANDROID) ... endif() guard and move the matching
set_source_files_properties(...) block under the same if(ANDROID) guard so both
the file additions and their property settings are only applied for Android
builds.
src/framework/core/minizip/ioapi.c (1)

99-110: Memory safety issues in vendored minizip code.

The file_build_ioposix function has potential NULL pointer dereferences if malloc fails:

  • Line 104: No NULL check after malloc(sizeof(FILE_IOPOSIX))
  • Line 107: No NULL check after malloc(...) for filename

If memory allocation fails, subsequent dereferences (lines 105-108) will crash. Similar patterns exist in fopendisk64_file_func and fopendisk_file_func.

Since this is vendored third-party code (minizip v1.2), these are known upstream issues. Consider either:

  1. Tracking upstream for fixes
  2. Applying a minimal defensive patch if Android builds run in memory-constrained environments
🛡️ Defensive patch for file_build_ioposix
 static voidpf file_build_ioposix(FILE *file, const char *filename)
 {
     FILE_IOPOSIX *ioposix = NULL;
     if (file == NULL)
         return NULL;
     ioposix = (FILE_IOPOSIX*)malloc(sizeof(FILE_IOPOSIX));
+    if (ioposix == NULL)
+        return NULL;
     ioposix->file = file;
     ioposix->filenameLength = (int)strlen(filename) + 1;
     ioposix->filename = (char*)malloc(ioposix->filenameLength * sizeof(char));
+    if (ioposix->filename == NULL) {
+        free(ioposix);
+        return NULL;
+    }
     strncpy((char*)ioposix->filename, filename, ioposix->filenameLength);
     return (voidpf)ioposix;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/ioapi.c` around lines 99 - 110, file_build_ioposix
(and similarly fopendisk64_file_func / fopendisk_file_func) can dereference NULL
if malloc fails; add defensive NULL checks: after allocating FILE_IOPOSIX
(ioposix = malloc(...)) verify ioposix != NULL and return NULL on failure, then
after allocating ioposix->filename verify it != NULL and if it is free(ioposix)
and return NULL; ensure you only compute/use ioposix->filenameLength and strncpy
when both allocations succeeded and preserve behavior by returning
(voidpf)ioposix on success.
src/framework/platform/androidwindow.cpp (1)

264-280: Potential logic issue in right-click detection.

The right-button assignment on long press (lines 274-277) occurs inside the TOUCH_UP branch, but mouseButton is used earlier on line 268 to set m_inputEvent.mouseButton. When a long press is detected, mouseButton is reassigned to Fw::MouseRightButton but this happens after the input event type was already set with Fw::MouseLeftButton. The subsequent re-assignment on line 276 fixes m_inputEvent.mouseButton, but this logic flow is confusing and error-prone.

Consider restructuring to determine the button type before setting any input event fields.

♻️ Suggested refactor
 void AndroidWindow::processFingerDownAndUp() {
     bool isTouchdown = m_currentEvent.type == TOUCH_DOWN;
 
-    Fw::MouseButton mouseButton = Fw::MouseLeftButton;
-
-    m_inputEvent.reset();
-    m_inputEvent.type = (isTouchdown) ? Fw::MousePressInputEvent : Fw::MouseReleaseInputEvent;
-    m_inputEvent.mouseButton = mouseButton;
+    Fw::MouseButton mouseButton = Fw::MouseLeftButton;
 
     if (isTouchdown) {
         m_lastPress = g_clock.millis();
-        m_mouseButtonStates |= 1 << mouseButton;
     } else if (m_currentEvent.type == TOUCH_UP) {
         if (!m_isDragging && stdext::millis() > m_lastPress + 500) {
             mouseButton = Fw::MouseRightButton;
-            m_inputEvent.mouseButton = mouseButton;
         }
         m_isDragging = false;
+    }
+
+    m_inputEvent.reset();
+    m_inputEvent.type = (isTouchdown) ? Fw::MousePressInputEvent : Fw::MouseReleaseInputEvent;
+    m_inputEvent.mouseButton = mouseButton;
+
+    if (isTouchdown) {
+        m_mouseButtonStates |= 1 << mouseButton;
+    } else {
         g_dispatcher.addEvent([this, mouseButton] { m_mouseButtonStates &= ~(1 << mouseButton); });
     }
 
     handleInputEvent();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/platform/androidwindow.cpp` around lines 264 - 280, Determine
the correct mouse button before writing any fields to m_inputEvent: compute
mouseButton based on isTouchdown (default MouseLeftButton) and, if !isTouchdown
and m_currentEvent.type == TOUCH_UP and stdext::millis() > m_lastPress + 500 and
!m_isDragging, set mouseButton = Fw::MouseRightButton; then set
m_inputEvent.type and m_inputEvent.mouseButton once using that computed
mouseButton. Keep the existing behaviors for updating m_lastPress, toggling
m_isDragging, updating m_mouseButtonStates (use |= when pressing and schedule &=
~(1 << mouseButton) via g_dispatcher.addEvent on release), but move those
updates so they use the precomputed mouseButton and avoid reassigning
m_inputEvent.mouseButton later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@build_luajit_android.sh`:
- Line 9: The script's use of "set -e" doesn't catch failures from piped
commands like the "make ... | tail -3" invocation, so a failed LuaJIT build can
still proceed to header installation and print a success banner; update
build_luajit_android.sh to either enable pipefail (e.g., set -euo pipefail) at
the top or explicitly check the exit status of the make pipeline (e.g., inspect
PIPESTATUS[0] or capture make's exit code via tee) after the "make ... | tail
-3" call and abort (non-zero exit) on failure before proceeding to the header
install and success message.

In `@Dockerfile.android`:
- Around line 24-36: The Dockerfile proceeds to run the Android Gradle build
(./gradlew_unix assembleRelease) without producing the required LuaJIT static
libraries (libluajit-5.1.a), which causes CMakeLists.txt to emit a FATAL_ERROR;
fix by adding a pre-build step in the Dockerfile to generate the LuaJIT
artifacts (invoke ./build_luajit_android.sh or the project's
./setup_android_deps.sh) after copying the repo and before the Gradle build so
that android/app/libs/lib/<ABI>/ contains libluajit-5.1.a for each ABI expected
by CMake.

In `@setup_android_deps.sh`:
- Around line 122-129: The script currently prints "Now run: cd android &&
./gradlew assembleRelease" but never creates the expected
android/app/src/main/assets/data.zip, causing AndroidManager::unZipAssetData()
to fail; update the end of the setup_android_deps.sh flow to mirror
Dockerfile.android by ensuring the assets directory exists and producing a
data.zip (e.g., cd into the data source directory and run zip -r into
android/app/src/main/assets/data.zip or invoke the same packaging step used in
Dockerfile.android) so the APK/assembleRelease step includes the asset that
AndroidManager::unZipAssetData() expects.
- Around line 55-65: Add the missing amalg build target and the TARGET_CFLAGS
with -DLUAJIT_UNWIND_EXTERNAL to the make invocation in setup_android_deps.sh so
it matches build_luajit_android.sh: update the make command that currently sets
HOST_CC, CROSS, STATIC_CC, DYNAMIC_CC, TARGET_LD, TARGET_AR, TARGET_STRIP,
TARGET_SYS, and BUILDMODE to also include TARGET_CFLAGS="${NDKBIN}/${CROSS}clang
-fPIC ? or the appropriate CFLAGS variable used in build_luajit_android.sh plus
-DLUAJIT_UNWIND_EXTERNAL" (use the same variable name TARGET_CFLAGS as in
build_luajit_android.sh) and append the amalg target to the make call so the
build uses the single-file amalgamated target for Android NDK C++ interop.
- Line 5: The script uses "set -e" but pipes the LuaJIT build (the "make ... |
tail -3" pipeline) and the "vcpkg install ... | tail -3" pipeline so failures
are masked by tail's success; enable pipe failure detection by adding "set -o
pipefail" near "set -e" or explicitly check the producer's exit status (e.g.,
inspect PIPESTATUS[0] after the "make ... | tail -3" and after the "vcpkg
install ... | tail -3") and exit if non‑zero; alternatively wrap those pipelines
in an if/then (if ! make ... | tail -3; then exit 1; fi) to ensure LuaJIT build
and vcpkg install failures stop the script.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 55-64: The function call_ztell64 has the wrong NULL check: it
tests pfilefunc->zfile_func64.zseek64_file but then calls ztell64_file, risking
a NULL-deref; update the condition to check pfilefunc->zfile_func64.ztell64_file
before calling it, and keep the fallback to pfilefunc->ztell32_file unchanged
(optionally ensure ztell32_file is non-NULL if desired); modify the check in
call_ztell64 so it verifies ztell64_file is not NULL and then invokes
ztell64_file(opaque, filestream).

In `@src/framework/core/minizip/unzip.c`:
- Around line 1487-1495: The variable flags is used uninitialized in the Apple
compression path; initialize it before use so it has a defined value when
s->pfile_in_zip_read->stream.avail_in != 0. For example, set flags to 0 (no
flags) by default, or explicitly set flags = 0 in the else branch, then call
compression_stream_process(&s->pfile_in_zip_read->astream, flags); this ensures
flags is defined for the call.

---

Outside diff comments:
In `@src/framework/core/unzipper.cpp`:
- Around line 86-103: Reject or sanitize zip entries that try to escape the
intended extraction root: build the extraction target using
std::filesystem::path dest = std::filesystem::path(destinationPath) / filename
and normalize it (e.g., dest = dest.lexically_normal()), compute the normalized
extraction root std::filesystem::path root =
std::filesystem::path(destinationPath).lexically_normal(), then verify dest is
not absolute and that dest.string() begins with root.string() (or use
std::mismatch/compare on path components) — if the check fails, log an error and
skip the entry instead of creating directories or writing the file; apply this
check before using destFilePath/destFilePath.parent_path(), before calling
create_directories(), and before opening the file after unzOpenCurrentFile in
unzipper.cpp.

---

Nitpick comments:
In `@src/CMakeLists.txt`:
- Around line 241-244: The four MiniZip C sources
(framework/core/minizip/ioapi.c, ioapi_mem.c, unzip.c, crypt.c) are being added
unconditionally; wrap the list(APPEND SOURCE_FILES ... ) that includes those
filenames in an if(ANDROID) ... endif() guard and move the matching
set_source_files_properties(...) block under the same if(ANDROID) guard so both
the file additions and their property settings are only applied for Android
builds.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 99-110: file_build_ioposix (and similarly fopendisk64_file_func /
fopendisk_file_func) can dereference NULL if malloc fails; add defensive NULL
checks: after allocating FILE_IOPOSIX (ioposix = malloc(...)) verify ioposix !=
NULL and return NULL on failure, then after allocating ioposix->filename verify
it != NULL and if it is free(ioposix) and return NULL; ensure you only
compute/use ioposix->filenameLength and strncpy when both allocations succeeded
and preserve behavior by returning (voidpf)ioposix on success.

In `@src/framework/platform/androidwindow.cpp`:
- Around line 264-280: Determine the correct mouse button before writing any
fields to m_inputEvent: compute mouseButton based on isTouchdown (default
MouseLeftButton) and, if !isTouchdown and m_currentEvent.type == TOUCH_UP and
stdext::millis() > m_lastPress + 500 and !m_isDragging, set mouseButton =
Fw::MouseRightButton; then set m_inputEvent.type and m_inputEvent.mouseButton
once using that computed mouseButton. Keep the existing behaviors for updating
m_lastPress, toggling m_isDragging, updating m_mouseButtonStates (use |= when
pressing and schedule &= ~(1 << mouseButton) via g_dispatcher.addEvent on
release), but move those updates so they use the precomputed mouseButton and
avoid reassigning m_inputEvent.mouseButton later.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbbac8d8-6a94-4e4e-b70d-4b940663c9f5

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5dfa4 and fb15559.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • android/app/src/main/AndroidManifest.xml
  • .dockerignore
  • android/app/build.gradle.kts
  • src/framework/core/minizip/crypt.h
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp
  • src/framework/platform/androidmanager.cpp
  • setup_android_wsl.sh
  • android/BUILDING.md

@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch 2 times, most recently from fdb92cd to c7ec6fb Compare March 26, 2026 07:01
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/framework/platform/androidwindow.cpp (1)

264-279: ⚠️ Potential issue | 🟠 Major

Long-press release leaves the left button stuck down.

Line 272 always marks Fw::MouseLeftButton as pressed, but Lines 274-279 switch only the release event to right-button and clear the right-bit. After a long press, m_mouseButtonStates still contains left.

Suggested patch
     if (isTouchdown) {
         m_lastPress = g_clock.millis();
-        m_mouseButtonStates |= 1 << mouseButton;
+        m_mouseButtonStates |= 1 << Fw::MouseLeftButton;
     } else if (m_currentEvent.type == TOUCH_UP) {
         if (!m_isDragging && stdext::millis() > m_lastPress + 500) {
             mouseButton = Fw::MouseRightButton;
             m_inputEvent.mouseButton = mouseButton;
         }
         m_isDragging = false;
-        g_dispatcher.addEvent([this, mouseButton] { m_mouseButtonStates &= ~(1 << mouseButton); });
+        g_dispatcher.addEvent([this] { m_mouseButtonStates &= ~(1 << Fw::MouseLeftButton); });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/platform/androidwindow.cpp` around lines 264 - 279, The release
path incorrectly always sets the initial mouseButton to Fw::MouseLeftButton and
later may reassign it to right for the input event but then clears the bit for
the reassigned button, leaving the left-bit stuck; fix by preserving the actual
pressed button (capture the pressedButton before you mutate mouseButton) or
compute the bit to clear from the originally set pressed button
(m_mouseButtonStates) rather than the possibly reassigned mouseButton, then use
that preserved value when calling m_mouseButtonStates &= ~(1 << ... ) inside the
g_dispatcher.addEvent block so the correct bit (left or right) is cleared after
release.
♻️ Duplicate comments (2)
setup_android_deps.sh (1)

55-65: ⚠️ Potential issue | 🟠 Major

Keep the LuaJIT Android build flags aligned with the dedicated builder.

This path still omits the amalg target and TARGET_CFLAGS="-fPIC -DLUAJIT_UNWIND_EXTERNAL -fno-stack-protector", so setup_android_deps.sh can emit different libluajit-5.1.a artifacts from build_luajit_android.sh for the same ABI.

Suggested patch
-    make -j$(nproc) \
+    make -j"$(nproc)" amalg \
         HOST_CC="$HOST_CC" \
         CROSS="${NDKBIN}/${CROSS}" \
         STATIC_CC="${NDKBIN}/${CROSS}clang -fPIC" \
         DYNAMIC_CC="${NDKBIN}/${CROSS}clang -fPIC" \
         TARGET_LD="${NDKBIN}/${CROSS}clang" \
         TARGET_AR="$NDKBIN/llvm-ar rcus" \
         TARGET_STRIP="$NDKBIN/llvm-strip" \
+        TARGET_CFLAGS="-fPIC -DLUAJIT_UNWIND_EXTERNAL -fno-stack-protector" \
         TARGET_SYS=Linux \
         BUILDMODE=static \
         2>&1 | tail -3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` around lines 55 - 65, The make invocation in
setup_android_deps.sh omits the LuaJIT-specific build settings causing different
artifacts; update the make command (the block invoking make -j$(nproc) with
HOST_CC/CROSS/STATIC_CC/etc.) to include the amalg target and set TARGET_CFLAGS
to "-fPIC -DLUAJIT_UNWIND_EXTERNAL -fno-stack-protector" so it matches
build_luajit_android.sh; ensure the final make arguments append "amalg" and
include TARGET_CFLAGS="...".
src/framework/core/minizip/unzip.c (1)

1487-1495: ⚠️ Potential issue | 🟠 Major

Initialize flags before calling libcompression.

When avail_in != 0, flags is never assigned before compression_stream_process().

Suggested patch
             compression_status status;
-            compression_stream_flags flags;
+            compression_stream_flags flags = 0;
 
             if (s->pfile_in_zip_read->stream.avail_in == 0)
             {
                 flags = COMPRESSION_STREAM_FINALIZE;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1487 - 1495, The variable
flags (type compression_stream_flags) may be read uninitialized when
s->pfile_in_zip_read->stream.avail_in != 0 before calling
compression_stream_process(&s->pfile_in_zip_read->astream, flags); initialize
flags to a safe default (e.g., 0 or COMPRESSION_STREAM_NO_FLAGS) before the if,
and keep the existing assignment to COMPRESSION_STREAM_FINALIZE inside the if so
flags is always defined when passed to compression_stream_process; update the
block around compression_status, compression_stream_flags, the avail_in check
and compression_stream_process call accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@setup_android_deps.sh`:
- Around line 127-145: The zip branch should mirror the Python branch by
creating a fresh archive of data/mods/modules and only adding init.lua and
otclientrc.lua if they exist; replace the single zip -r
android/app/src/main/assets/data.zip data mods modules init.lua otclientrc.lua
invocation with a sequence that (inside $SCRIPT_DIR) creates/overwrites
android/app/src/main/assets/data.zip from directories data, mods, modules and
then conditionally adds init.lua and otclientrc.lua only when they exist (using
zip -r for the directories and zip -u or additional zip calls guarded by [ -e
"$f" ] for each optional file) so the behavior matches the python block and the
script does not abort when optional files are missing.

In `@src/CMakeLists.txt`:
- Around line 204-212: The Android minizip sources are being lost because a
later set(SOURCE_FILES ...) overrides the earlier list append; create a separate
variable (e.g., ANDROID_MINIZIP_SOURCES) to hold the minizip file list instead
of directly appending to SOURCE_FILES inside the Android conditional, and after
the main set(SOURCE_FILES ...) block but before add_library(otclient_core STATIC
${SOURCE_FILES}) conditionally list(APPEND SOURCE_FILES
${ANDROID_MINIZIP_SOURCES}) when ANDROID is true so the minizip files are
preserved for the final SOURCE_FILES used by add_library.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 158-180: In fopendisk64_file_func (and the similar
fopendisk_file_func at 183-205) guard the diskFilename allocation and string
ops: check malloc returned non-NULL before calling strncpy/snprintf and return
NULL (or handle error) if allocation fails; ensure you allocate space for a
terminating NUL (use filenameLength+1) and explicitly NUL-terminate diskFilename
after copying; only call snprintf if diskFilename is non-NULL and the remaining
buffer size (filenameLength - i) is correct; free diskFilename on all paths and
call fopen64_file_func/fopen_file_func only when diskFilename is valid.

In `@src/framework/core/minizip/unzip.c`:
- Around line 1590-1598: The code in unzGetLocalExtrafield (using
s->pfile_in_zip_read, ZSEEK64 and ZREAD64) fails to account for
byte_before_the_zipfile when seeking and does not advance pos_local_extrafield
after a partial read, causing misaligned or repeated reads for archives with
prepended data and chunked callers. Fix by adding byte_before_the_zipfile to the
seek offset (i.e. include s->pfile_in_zip_read->byte_before_the_zipfile when
computing the ZSEEK64 target) and after a successful ZREAD64 increment
s->pfile_in_zip_read->pos_local_extrafield by the number of bytes read (read_now
or the actual bytes returned) so subsequent calls continue from the right
position; ensure the function returns the actual bytes read and handles partial
reads correctly.
- Around line 511-519: The bridge struct zlib_filefunc64_32_def_fill in
unzOpen2_64 is not fully initialized, leaving zopen32_file and zopendisk32_file
indeterminate; zero-initialize the entire zlib_filefunc64_32_def_fill (e.g.,
memset or aggregate initialization) before setting zfile_func64 and the 32-bit
callbacks, and explicitly set zopen32_file and zopendisk32_file to NULL so
unzOpenInternal sees defined fallbacks.
- Around line 1752-1776: The restore block after a failed locate restores
num_file, pos_in_central_dir, cur_file_info and cur_file_info_internal but omits
s->current_file_ok; update the restore logic to save the original
s->current_file_ok into a local variable (e.g., current_file_ok_saved) before
scanning and then set s->current_file_ok = current_file_ok_saved when restoring,
ensuring functions that call unzGoToFirstFile2/unzGoToNextFile2 can rely on the
previous current file validity.
- Around line 1915-1970: unzSeek64 rebuilds the cursor incorrectly: the reset
path sets pos_in_zipfile to offset_local_extrafield + position (pointing into
the local extra field) and the fast buffered path uses unsigned arithmetic so
backward seeks wrap. Fix by computing a signed delta = (int64_t)position -
(int64_t)s->pfile_in_zip_read->total_out_64 and only take the buffered fast path
when delta >= 0 and delta fits within stream.avail_in/stream window; otherwise
clear the buffer and set pos_in_zipfile to the actual start-of-file-data plus
position (use the field that represents the file data start rather than
offset_local_extrafield, e.g., a stored start-of-data offset or compute from
local header), set rest_read_compressed = cur_file_info.compressed_size -
position, and update rest_read_uncompressed and total_out fields using the
signed delta.

In `@src/framework/core/unzipper.cpp`:
- Around line 85-93: The unsafe-path branch in unzipper.cpp calls
unzCloseCurrentFile() before unzOpenCurrentFile() (violating MiniZip's
lifecycle) and ignores errors from unzGoToNextFile(); remove the
unzCloseCurrentFile() call from the unsafe-path branch, call unzGoToNextFile()
and check its return value (like the normal iteration path), and on failure log
an error via g_logger and abort/return an error status (or break the loop) to
avoid silent failures; reference the unsafe-path branch surrounding entryPath
checks, unzOpenCurrentFile, unzCloseCurrentFile, and unzGoToNextFile to locate
the changes.

---

Outside diff comments:
In `@src/framework/platform/androidwindow.cpp`:
- Around line 264-279: The release path incorrectly always sets the initial
mouseButton to Fw::MouseLeftButton and later may reassign it to right for the
input event but then clears the bit for the reassigned button, leaving the
left-bit stuck; fix by preserving the actual pressed button (capture the
pressedButton before you mutate mouseButton) or compute the bit to clear from
the originally set pressed button (m_mouseButtonStates) rather than the possibly
reassigned mouseButton, then use that preserved value when calling
m_mouseButtonStates &= ~(1 << ... ) inside the g_dispatcher.addEvent block so
the correct bit (left or right) is cleared after release.

---

Duplicate comments:
In `@setup_android_deps.sh`:
- Around line 55-65: The make invocation in setup_android_deps.sh omits the
LuaJIT-specific build settings causing different artifacts; update the make
command (the block invoking make -j$(nproc) with HOST_CC/CROSS/STATIC_CC/etc.)
to include the amalg target and set TARGET_CFLAGS to "-fPIC
-DLUAJIT_UNWIND_EXTERNAL -fno-stack-protector" so it matches
build_luajit_android.sh; ensure the final make arguments append "amalg" and
include TARGET_CFLAGS="...".

In `@src/framework/core/minizip/unzip.c`:
- Around line 1487-1495: The variable flags (type compression_stream_flags) may
be read uninitialized when s->pfile_in_zip_read->stream.avail_in != 0 before
calling compression_stream_process(&s->pfile_in_zip_read->astream, flags);
initialize flags to a safe default (e.g., 0 or COMPRESSION_STREAM_NO_FLAGS)
before the if, and keep the existing assignment to COMPRESSION_STREAM_FINALIZE
inside the if so flags is always defined when passed to
compression_stream_process; update the block around compression_status,
compression_stream_flags, the avail_in check and compression_stream_process call
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 727ce52a-6ea0-45fa-b18a-bbb8fe0adac7

📥 Commits

Reviewing files that changed from the base of the PR and between fb15559 and fdb92cd.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp
✅ Files skipped from review due to trivial changes (7)
  • src/framework/platform/androidmanager.cpp
  • .gitignore
  • src/framework/ui/uitextedit.cpp
  • .dockerignore
  • android/app/src/main/AndroidManifest.xml
  • android/BUILDING.md
  • src/framework/core/minizip/crypt.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • android/app/build.gradle.kts
  • setup_android_wsl.sh

@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch from c7ec6fb to dc7c9fb Compare March 26, 2026 15:25
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (9)
src/framework/core/minizip/ioapi.c (1)

158-180: ⚠️ Potential issue | 🟡 Minor

Guard diskFilename before the first copy.

Both spanning helpers still dereference diskFilename immediately after malloc(). On allocation failure, that becomes a null-pointer crash.

🛠️ Suggested fix
     ioposix = (FILE_IOPOSIX*)stream;
     diskFilename = (char*)malloc(ioposix->filenameLength * sizeof(char));
+    if (diskFilename == NULL)
+        return NULL;
     strncpy(diskFilename, (const char*)ioposix->filename, ioposix->filenameLength);
     ioposix = (FILE_IOPOSIX*)stream;
     diskFilename = (char*)malloc(ioposix->filenameLength * sizeof(char));
+    if (diskFilename == NULL)
+        return NULL;
     strncpy(diskFilename, (const char*)ioposix->filename, ioposix->filenameLength);

Also applies to: 183-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/ioapi.c` around lines 158 - 180, The code in
fopendisk64_file_func (and the similar fopendisk_file_func) uses diskFilename
immediately after malloc without checking for allocation failure, which can
dereference NULL; fix by testing the return of malloc for NULL before calling
strncpy/strncpy/snprintf and if allocation fails return NULL (or appropriate
error) and avoid calling fopen64_file_func/fopen_file_func; ensure you only free
diskFilename when non-NULL and propagate the error path consistently in both
functions.
src/framework/core/unzipper.cpp (1)

85-92: ⚠️ Potential issue | 🟠 Major

Don't close a MiniZip entry that was never opened.

This branch runs before Line 103 calls unzOpenCurrentFile(), so unzCloseCurrentFile() here is invalid. The unchecked unzGoToNextFile() also makes a bad iteration state harder to diagnose.

🛠️ Suggested fix
 if (entryPath.is_absolute() || entryPath.string().starts_with("..")) {
     g_logger.warning("skipping zip entry with unsafe path: {}", filename);
-    unzCloseCurrentFile( zipfile );
-    if ( ( i+1 ) < global_info.number_entry )
-        unzGoToNextFile( zipfile );
+    if ( ( i+1 ) < global_info.number_entry &&
+         unzGoToNextFile( zipfile ) != UNZ_OK )
+    {
+        unzClose( zipfile );
+        g_logger.fatal("could not read next file");
+    }
     continue;
 }
MiniZip classic API: is `unzCloseCurrentFile()` valid before `unzOpenCurrentFile()`, and should `unzGoToNextFile()` failures be checked when skipping an entry?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/unzipper.cpp` around lines 85 - 92, The branch that
rejects unsafe entry paths must not call unzCloseCurrentFile() because
unzOpenCurrentFile() hasn't been called yet; remove the
unzCloseCurrentFile(zipfile) call (or only call it when a file was actually
opened), and when advancing with unzGoToNextFile(zipfile) check its return value
and log or handle errors (use zipfile, i, global_info.number_entry) instead of
ignoring failures; keep the existing g_logger.warning(...) and use
entryPath/filename to report context.
setup_android_deps.sh (2)

55-65: ⚠️ Potential issue | 🟠 Major

Match this LuaJIT build with the documented Android builder.

Line 55 still omits amalg and TARGET_CFLAGS=-DLUAJIT_UNWIND_EXTERNAL, so setup_android_deps.sh can produce a different libluajit-5.1.a than build_luajit_android.sh.

🛠️ Suggested patch
-    make -j$(nproc) \
+    make -j"$(nproc)" amalg \
         HOST_CC="$HOST_CC" \
         CROSS="${NDKBIN}/${CROSS}" \
         STATIC_CC="${NDKBIN}/${CROSS}clang -fPIC" \
         DYNAMIC_CC="${NDKBIN}/${CROSS}clang -fPIC" \
         TARGET_LD="${NDKBIN}/${CROSS}clang" \
         TARGET_AR="$NDKBIN/llvm-ar rcus" \
         TARGET_STRIP="$NDKBIN/llvm-strip" \
+        TARGET_CFLAGS="-fPIC -DLUAJIT_UNWIND_EXTERNAL -fno-stack-protector" \
         TARGET_SYS=Linux \
         BUILDMODE=static \
         2>&1 | tail -3
#!/bin/bash
# Compare the LuaJIT make invocations used by the two Android build paths.
echo "=== setup_android_deps.sh ==="
sed -n '52,66p' setup_android_deps.sh

echo
echo "=== build_luajit_android.sh ==="
fd '^build_luajit_android\.sh$' -x sed -n '1,220p' {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` around lines 55 - 65, The LuaJIT make invocation in
setup_android_deps.sh is missing the amalg build flag and
TARGET_CFLAGS=-DLUAJIT_UNWIND_EXTERNAL so it can produce a different
libluajit-5.1.a than build_luajit_android.sh; update the make call (the
multi-line make block that sets HOST_CC, CROSS, STATIC_CC, DYNAMIC_CC,
TARGET_LD, TARGET_AR, TARGET_STRIP, TARGET_SYS, BUILDMODE) to include amalg and
TARGET_CFLAGS=-DLUAJIT_UNWIND_EXTERNAL so the build flags match the documented
Android builder.

127-142: ⚠️ Potential issue | 🟠 Major

Recreate data.zip and only add optional root files when they exist.

Line 130 updates any existing archive in place and fails the whole script if init.lua or otclientrc.lua is missing. The Python fallback already treats both as optional, so the zip path is currently less reliable than the fallback.

🛠️ Suggested patch
 if command -v zip &>/dev/null; then
-    zip -r android/app/src/main/assets/data.zip data mods modules init.lua otclientrc.lua
+    rm -f android/app/src/main/assets/data.zip
+    inputs=(data mods modules)
+    [[ -f init.lua ]] && inputs+=(init.lua)
+    [[ -f otclientrc.lua ]] && inputs+=(otclientrc.lua)
+    zip -r android/app/src/main/assets/data.zip "${inputs[@]}"
 elif command -v python3 &>/dev/null; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` around lines 127 - 142, The zip invocation currently
updates an existing archive and fails if optional root files are missing; change
the shell branch that runs zip to recreate the archive and only add init.lua and
otclientrc.lua if they exist (mirror the Python fallback). Concretely, before
calling zip in the shell branch remove any existing
android/app/src/main/assets/data.zip, then run zip -r to add the directories
data, mods, modules and append init.lua and otclientrc.lua only when test -f
returns true (e.g. use $(test -f init.lua && echo init.lua) and similar for
otclientrc.lua). Update the block containing the zip command (the shell branch
that parallels the Python fallback) so behavior matches the Python fallback.
src/framework/core/minizip/unzip.c (5)

511-519: ⚠️ Potential issue | 🟠 Major

Zero-initialize the 64/32 callback bridge.

Line 515 leaves zopen32_file and zopendisk32_file indeterminate. If a caller provides a partial zlib_filefunc64_def, the fallback path can jump through garbage.

🛠️ Suggested patch
     if (pzlib_filefunc_def != NULL)
     {
-        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill = {0};
         zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
-        zlib_filefunc64_32_def_fill.ztell32_file = NULL;
-        zlib_filefunc64_32_def_fill.zseek32_file = NULL;
         return unzOpenInternal(path, &zlib_filefunc64_32_def_fill);
     }
#!/bin/bash
# Inspect the bridge initialization and the fallback dispatch in ioapi.c.
echo "=== unzOpen2_64 bridge setup ==="
sed -n '511,519p' src/framework/core/minizip/unzip.c

echo
echo "=== call_zopen64 / call_zopendisk64 fallback logic ==="
fd '^ioapi\.c$' -x rg -n -C3 'call_zopen64|call_zopendisk64|zopen32_file|zopendisk32_file' {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 511 - 519, unzOpen2_64
currently leaves members of the bridge struct zlib_filefunc64_32_def_fill
(notably zopen32_file and zopendisk32_file) uninitialized; zero-initialize the
whole zlib_filefunc64_32_def_fill before assigning zfile_func64 and the other
members so any missing 64-bit callbacks safely fall back to NULL, e.g. clear the
struct (memset or initializer) and then set zfile_func64 = *pzlib_filefunc_def
and ztell32_file/zseek32_file = NULL to ensure call_zopen64/call_zopendisk64
won't execute garbage.

1487-1495: ⚠️ Potential issue | 🟠 Major

Initialize flags before compression_stream_process.

Line 1488 leaves flags undefined whenever avail_in != 0, which makes the Apple decompression path undefined behavior.

🛠️ Suggested patch
             compression_status status;
-            compression_stream_flags flags;
+            compression_stream_flags flags = 0;

             if (s->pfile_in_zip_read->stream.avail_in == 0)
             {
                 flags = COMPRESSION_STREAM_FINALIZE;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1487 - 1495, The variable
`flags` (type compression_stream_flags) is uninitialized when
`s->pfile_in_zip_read->stream.avail_in != 0`; ensure `flags` is given a defined
value before calling `compression_stream_process` (e.g. initialize it to 0 or
explicitly set it in an else branch) so that the call to
`compression_stream_process(&s->pfile_in_zip_read->astream, flags)` always
receives a defined flag value; update the code around the `if
(s->pfile_in_zip_read->stream.avail_in == 0)` check (referencing `flags`,
`compression_stream_process`, `compression_stream_flags`, and
`s->pfile_in_zip_read->stream.avail_in`) to set a default/else value.

1590-1598: ⚠️ Potential issue | 🟠 Major

Advance the local-extra-field cursor and include the prepended-data offset.

Lines 1591-1598 reread the wrong bytes for self-extracting archives and for chunked callers because byte_before_the_zipfile is skipped and pos_local_extrafield never advances.

🛠️ Suggested patch
     if (ZSEEK64(s->pfile_in_zip_read->z_filefunc, s->pfile_in_zip_read->filestream,
-        s->pfile_in_zip_read->offset_local_extrafield + s->pfile_in_zip_read->pos_local_extrafield,
+        s->pfile_in_zip_read->offset_local_extrafield +
+        s->pfile_in_zip_read->byte_before_the_zipfile +
+        s->pfile_in_zip_read->pos_local_extrafield,
         ZLIB_FILEFUNC_SEEK_SET) != 0)
         return UNZ_ERRNO;

     if (ZREAD64(s->pfile_in_zip_read->z_filefunc, s->pfile_in_zip_read->filestream, buf, read_now) != read_now)
         return UNZ_ERRNO;

+    s->pfile_in_zip_read->pos_local_extrafield += read_now;
     return (int)read_now;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1590 - 1598, The code in
unzip.c (inside the read-from-local-extra-field sequence that uses
ZSEEK64/ZREAD64) fails to include the archive's byte_before_the_zipfile and does
not advance pos_local_extrafield, causing wrong bytes to be read for
self-extracting archives and chunked callers; fix by seeking to filestream
position (byte_before_the_zipfile + offset_local_extrafield +
pos_local_extrafield) before reading and after a successful ZREAD64 increment
s->pfile_in_zip_read->pos_local_extrafield by read_now (or the actual bytes
read) so subsequent reads continue from the correct local-extra-field cursor,
ensuring UNZ_ERRNO is still returned on seek/read failures and the function
returns (int)read_now on success.

1752-1776: ⚠️ Potential issue | 🟠 Major

Restore current_file_ok after a failed locate.

Line 1771 restores the cursor state but not the validity flag. After one miss, the previously current entry becomes unusable until the caller repositions manually.

🛠️ Suggested patch
     uint64_t num_file_saved = 0;
     uint64_t pos_in_central_dir_saved = 0;
+    uint64_t current_file_ok_saved = 0;
     char current_filename[UNZ_MAXFILENAMEINZIP+1];
     int err = UNZ_OK;
@@
     num_file_saved = s->num_file;
     pos_in_central_dir_saved = s->pos_in_central_dir;
     cur_file_info_saved = s->cur_file_info;
     cur_file_info_internal_saved = s->cur_file_info_internal;
+    current_file_ok_saved = s->current_file_ok;
@@
     s->num_file = num_file_saved;
     s->pos_in_central_dir = pos_in_central_dir_saved;
     s->cur_file_info = cur_file_info_saved;
     s->cur_file_info_internal = cur_file_info_internal_saved;
+    s->current_file_ok = current_file_ok_saved;
     return err;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1752 - 1776, The code that
searches for a filename saves and restores s->num_file, s->pos_in_central_dir,
s->cur_file_info and s->cur_file_info_internal but misses s->current_file_ok, so
after a failed locate the validity flag remains clobbered; update the locate
routine (the block that calls unzGoToFirstFile2/unzGoToNextFile2) to save the
current value of s->current_file_ok into a local (e.g., current_file_ok_saved)
before the search and restore s->current_file_ok to that saved value in the same
restore section alongside num_file_saved, pos_in_central_dir_saved,
cur_file_info_saved and cur_file_info_internal_saved so the previous "current"
entry remains usable after a failed locate.

1912-1970: ⚠️ Potential issue | 🟠 Major

Reset stored-file seeks to the data start, not the local extra field.

Line 1964 rebuilds pos_in_zipfile from offset_local_extrafield, so non-trivial seeks restart inside the local extra field. Line 1968 also subtracts an unsigned delta, which underflows on backward seeks.

🛠️ Safe short-term fix
-    uint64_t stream_pos_begin = 0;
-    uint64_t stream_pos_end = 0;
     uint64_t position = 0;
-    int is_within_buffer = 0;
@@
-    stream_pos_end = s->pfile_in_zip_read->pos_in_zipfile;
-    stream_pos_begin = stream_pos_end;
-
-    if (stream_pos_begin > UNZ_BUFSIZE)
-        stream_pos_begin -= UNZ_BUFSIZE;
-    else
-        stream_pos_begin = 0;
-
-    is_within_buffer =
-        (s->pfile_in_zip_read->stream.avail_in != 0) &&
-        (s->pfile_in_zip_read->rest_read_compressed != 0 || s->cur_file_info.compressed_size < UNZ_BUFSIZE) &&
-        (position >= stream_pos_begin && position < stream_pos_end);
-
-    if (is_within_buffer)
-    {
-        s->pfile_in_zip_read->stream.next_in += position - s->pfile_in_zip_read->total_out_64;
-        s->pfile_in_zip_read->stream.avail_in = (uInt)(stream_pos_end - position);
-    }
-    else
-    {
-        s->pfile_in_zip_read->stream.avail_in = 0;
-        s->pfile_in_zip_read->stream.next_in = 0;
-
-        s->pfile_in_zip_read->pos_in_zipfile = s->pfile_in_zip_read->offset_local_extrafield + position;
-        s->pfile_in_zip_read->rest_read_compressed = s->cur_file_info.compressed_size - position;
-    }
-
-    s->pfile_in_zip_read->rest_read_uncompressed -= (position - s->pfile_in_zip_read->total_out_64);
+    s->pfile_in_zip_read->stream.avail_in = 0;
+    s->pfile_in_zip_read->stream.next_in = 0;
+    s->pfile_in_zip_read->pos_in_zipfile =
+        s->pfile_in_zip_read->offset_local_extrafield +
+        s->pfile_in_zip_read->size_local_extrafield +
+        position;
+    s->pfile_in_zip_read->rest_read_compressed = s->cur_file_info.compressed_size - position;
+    s->pfile_in_zip_read->rest_read_uncompressed = s->cur_file_info.uncompressed_size - position;
     s->pfile_in_zip_read->stream.total_out = (uint32_t)position;
     s->pfile_in_zip_read->total_out_64 = position;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1912 - 1970, The code resets
pos_in_zipfile to offset_local_extrafield which positions seeks inside the local
extra field and also subtracts an unsigned delta causing underflow on backward
seeks; change the seek base to the actual compressed-data start (compute from
the local header start + local header size / filename+extra lengths instead of
offset_local_extrafield) when setting s->pfile_in_zip_read->pos_in_zipfile, and
compute the moved amount as a signed difference (int64_t delta =
(int64_t)position - (int64_t)s->pfile_in_zip_read->total_out_64) then adjust
s->pfile_in_zip_read->rest_read_uncompressed using that signed delta to avoid
underflow; update references in unzSeek64 to use these corrected values
(pos_in_zipfile and rest_read_uncompressed adjustments).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@build_luajit_android.sh`:
- Around line 22-26: The current probe uses a compile-only check with "gcc -m32
-x c -c /dev/null -o /dev/null" which can pass even if the 32-bit linker or
libraries are missing; replace it with a full link test by invoking gcc -m32 to
compile-and-link a trivial C program (the same check that will be needed by
HOST_CC when building buildvm) and, if that link fails, install
gcc-multilib/g++-multilib; update the probe that references "gcc -m32 -x c -c
/dev/null -o /dev/null" and ensure it mirrors the actual invocation used for
HOST_CC so failures are detected early.

In `@Dockerfile.android`:
- Around line 11-14: Replace the open-ended git clone of the vcpkg repo with a
pinned checkout so the vcpkg tool is reproducible: after cloning
https://github.com/microsoft/vcpkg.git in the WORKDIR /opt step, perform a git
checkout of a specific commit hash (chosen to align with the vcpkg.json
builtin-baseline) before running ./bootstrap-vcpkg.sh; update the commit hash
when intentionally upgrading the tool. Reference: the RUN step that currently
does "git clone ... vcpkg.git && cd vcpkg && ./bootstrap-vcpkg.sh".

In `@src/framework/core/minizip/ioapi_mem.c`:
- Around line 68-79: fread_mem_func currently clamps reads against mem->size
(allocated capacity) which can return unwritten bytes after a grow/write cycle;
change the clamp to use the written-data boundary mem->limit instead (i.e.,
compute allowed = mem->limit - mem->cur_offset and reduce size = min(size,
allowed)), ensure you handle cases where cur_offset >= mem->limit by returning
0, and update mem->cur_offset only by the actual bytes copied.
- Around line 44-59: In fopen_mem_func, fail fast when the initial grow-buffer
allocation fails: after attempting to allocate mem->base (inside the mem->grow
branch) check whether malloc returned NULL, and if so free any allocated mem
state and return NULL instead of continuing; only set mem->size, mem->limit (and
later mem->cur_offset) and return mem when mem->base is non-NULL. Ensure the
NULL-check references mem->base and adjusts control flow so subsequent memcpy
calls cannot run on a NULL buffer.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 100-117: file_build_ioposix currently returns NULL on allocation
failures but leaves the incoming FILE* open; change the error paths so the FILE*
is closed before returning. Specifically, in file_build_ioposix: if
malloc(sizeof(FILE_IOPOSIX)) returns NULL, call fclose(file) then return NULL;
if malloc for ioposix->filename fails, free(ioposix), call fclose(file), then
return NULL. Apply the same pattern to the other open-helper(s) that allocate
FILE_IOPOSIX and ioposix->filename so no FILE* is leaked.

---

Duplicate comments:
In `@setup_android_deps.sh`:
- Around line 55-65: The LuaJIT make invocation in setup_android_deps.sh is
missing the amalg build flag and TARGET_CFLAGS=-DLUAJIT_UNWIND_EXTERNAL so it
can produce a different libluajit-5.1.a than build_luajit_android.sh; update the
make call (the multi-line make block that sets HOST_CC, CROSS, STATIC_CC,
DYNAMIC_CC, TARGET_LD, TARGET_AR, TARGET_STRIP, TARGET_SYS, BUILDMODE) to
include amalg and TARGET_CFLAGS=-DLUAJIT_UNWIND_EXTERNAL so the build flags
match the documented Android builder.
- Around line 127-142: The zip invocation currently updates an existing archive
and fails if optional root files are missing; change the shell branch that runs
zip to recreate the archive and only add init.lua and otclientrc.lua if they
exist (mirror the Python fallback). Concretely, before calling zip in the shell
branch remove any existing android/app/src/main/assets/data.zip, then run zip -r
to add the directories data, mods, modules and append init.lua and
otclientrc.lua only when test -f returns true (e.g. use $(test -f init.lua &&
echo init.lua) and similar for otclientrc.lua). Update the block containing the
zip command (the shell branch that parallels the Python fallback) so behavior
matches the Python fallback.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 158-180: The code in fopendisk64_file_func (and the similar
fopendisk_file_func) uses diskFilename immediately after malloc without checking
for allocation failure, which can dereference NULL; fix by testing the return of
malloc for NULL before calling strncpy/strncpy/snprintf and if allocation fails
return NULL (or appropriate error) and avoid calling
fopen64_file_func/fopen_file_func; ensure you only free diskFilename when
non-NULL and propagate the error path consistently in both functions.

In `@src/framework/core/minizip/unzip.c`:
- Around line 511-519: unzOpen2_64 currently leaves members of the bridge struct
zlib_filefunc64_32_def_fill (notably zopen32_file and zopendisk32_file)
uninitialized; zero-initialize the whole zlib_filefunc64_32_def_fill before
assigning zfile_func64 and the other members so any missing 64-bit callbacks
safely fall back to NULL, e.g. clear the struct (memset or initializer) and then
set zfile_func64 = *pzlib_filefunc_def and ztell32_file/zseek32_file = NULL to
ensure call_zopen64/call_zopendisk64 won't execute garbage.
- Around line 1487-1495: The variable `flags` (type compression_stream_flags) is
uninitialized when `s->pfile_in_zip_read->stream.avail_in != 0`; ensure `flags`
is given a defined value before calling `compression_stream_process` (e.g.
initialize it to 0 or explicitly set it in an else branch) so that the call to
`compression_stream_process(&s->pfile_in_zip_read->astream, flags)` always
receives a defined flag value; update the code around the `if
(s->pfile_in_zip_read->stream.avail_in == 0)` check (referencing `flags`,
`compression_stream_process`, `compression_stream_flags`, and
`s->pfile_in_zip_read->stream.avail_in`) to set a default/else value.
- Around line 1590-1598: The code in unzip.c (inside the
read-from-local-extra-field sequence that uses ZSEEK64/ZREAD64) fails to include
the archive's byte_before_the_zipfile and does not advance pos_local_extrafield,
causing wrong bytes to be read for self-extracting archives and chunked callers;
fix by seeking to filestream position (byte_before_the_zipfile +
offset_local_extrafield + pos_local_extrafield) before reading and after a
successful ZREAD64 increment s->pfile_in_zip_read->pos_local_extrafield by
read_now (or the actual bytes read) so subsequent reads continue from the
correct local-extra-field cursor, ensuring UNZ_ERRNO is still returned on
seek/read failures and the function returns (int)read_now on success.
- Around line 1752-1776: The code that searches for a filename saves and
restores s->num_file, s->pos_in_central_dir, s->cur_file_info and
s->cur_file_info_internal but misses s->current_file_ok, so after a failed
locate the validity flag remains clobbered; update the locate routine (the block
that calls unzGoToFirstFile2/unzGoToNextFile2) to save the current value of
s->current_file_ok into a local (e.g., current_file_ok_saved) before the search
and restore s->current_file_ok to that saved value in the same restore section
alongside num_file_saved, pos_in_central_dir_saved, cur_file_info_saved and
cur_file_info_internal_saved so the previous "current" entry remains usable
after a failed locate.
- Around line 1912-1970: The code resets pos_in_zipfile to
offset_local_extrafield which positions seeks inside the local extra field and
also subtracts an unsigned delta causing underflow on backward seeks; change the
seek base to the actual compressed-data start (compute from the local header
start + local header size / filename+extra lengths instead of
offset_local_extrafield) when setting s->pfile_in_zip_read->pos_in_zipfile, and
compute the moved amount as a signed difference (int64_t delta =
(int64_t)position - (int64_t)s->pfile_in_zip_read->total_out_64) then adjust
s->pfile_in_zip_read->rest_read_uncompressed using that signed delta to avoid
underflow; update references in unzSeek64 to use these corrected values
(pos_in_zipfile and rest_read_uncompressed adjustments).

In `@src/framework/core/unzipper.cpp`:
- Around line 85-92: The branch that rejects unsafe entry paths must not call
unzCloseCurrentFile() because unzOpenCurrentFile() hasn't been called yet;
remove the unzCloseCurrentFile(zipfile) call (or only call it when a file was
actually opened), and when advancing with unzGoToNextFile(zipfile) check its
return value and log or handle errors (use zipfile, i, global_info.number_entry)
instead of ignoring failures; keep the existing g_logger.warning(...) and use
entryPath/filename to report context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59d7e979-c734-4c87-a098-89b9d9ab626e

📥 Commits

Reviewing files that changed from the base of the PR and between fdb92cd and dc7c9fb.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp
✅ Files skipped from review due to trivial changes (6)
  • src/framework/ui/uitextedit.cpp
  • android/app/src/main/AndroidManifest.xml
  • android/app/build.gradle.kts
  • android/BUILDING.md
  • setup_android_wsl.sh
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/framework/platform/androidmanager.cpp
  • .dockerignore
  • src/main.cpp
  • src/framework/platform/androidwindow.cpp

@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch 2 times, most recently from 0b4287e to 6c174e6 Compare March 26, 2026 15:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/framework/platform/androidwindow.cpp (1)

264-279: ⚠️ Potential issue | 🟠 Major

Long-press currently leaves the left button pressed.

Line 272 always marks Fw::MouseLeftButton as pressed, but Lines 274-279 can rewrite the release to Fw::MouseRightButton and clear that bit instead. After a long-press there is no matching left-button release, and there is still no matching right-button press event. Clear the button that was actually pressed, or synthesize a full right-click pair instead of remapping only the release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/platform/androidwindow.cpp` around lines 264 - 279, The code
always sets mouseButton = Fw::MouseLeftButton and updates m_mouseButtonStates as
if left was pressed, then later may remap the release to Fw::MouseRightButton
leaving the left bit stuck; fix by recording the actually-pressed button at
touch-down (e.g. store an originalPressed variable or set an initialPressed
value before potential remap) and use that stored value when clearing
m_mouseButtonStates in the g_dispatcher callback (or alternatively, when you
remap a release to Fw::MouseRightButton synthesize a matching right-button press
event before scheduling the release), updating m_inputEvent.mouseButton only for
the delivered event while always clearing the bit for the button that was
originally set (references: Fw::MouseLeftButton, Fw::MouseRightButton,
m_inputEvent, m_mouseButtonStates, m_lastPress, TOUCH_UP, m_isDragging,
g_dispatcher).
♻️ Duplicate comments (10)
build_luajit_android.sh (1)

22-26: ⚠️ Potential issue | 🟠 Major

Use a link test for the gcc -m32 probe.

This check only compiles. HOST_CC="gcc -m32" later has to link buildvm, so a host missing 32-bit linker/libs can pass here and still fail inside make.

Suggested fix
-# Verify gcc -m32 works (needed for 32-bit targets)
-if ! gcc -m32 -x c -c /dev/null -o /dev/null 2>/dev/null; then
+# Verify gcc -m32 can compile and link host tools (needed for buildvm)
+tmp_bin="$(mktemp)"
+if ! printf 'int main(void){return 0;}\n' | gcc -m32 -x c - -o "$tmp_bin" 2>/dev/null; then
     echo "Installing gcc-multilib for 32-bit cross-compilation..."
     sudo apt-get install -y gcc-multilib g++-multilib
 fi
+rm -f "$tmp_bin"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build_luajit_android.sh` around lines 22 - 26, Current probe only compiles
with gcc -m32 and can miss missing 32-bit linker/libs; replace the probe with a
full link test (compile+link a trivial C program) to mirror how HOST_CC="gcc
-m32" will be used when building buildvm so failures are caught early; update
the check that currently runs `gcc -m32 -x c -c /dev/null` to instead attempt
compiling and linking a tiny main() and only install gcc-multilib/g++-multilib
if that link step fails.
src/framework/core/unzipper.cpp (1)

87-91: ⚠️ Potential issue | 🟡 Minor

Handle unzGoToNextFile() failures in the unsafe-entry branch.

If the cursor advance on Line 89 fails, the loop still continues as if the archive state moved, so extraction can silently proceed on stale MiniZip state. Check the return code here the same way you do in the normal iteration path.

Suggested fix
         if (entryPath.is_absolute() || entryPath.string().starts_with("..")) {
             g_logger.warning("skipping zip entry with unsafe path: {}", filename);
-            if ( ( i+1 ) < global_info.number_entry )
-                unzGoToNextFile( zipfile );
+            if ((i + 1) < global_info.number_entry &&
+                unzGoToNextFile(zipfile) != UNZ_OK) {
+                unzClose(zipfile);
+                g_logger.fatal("could not read next file");
+            }
             continue;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/unzipper.cpp` around lines 87 - 91, The unsafe-entry
branch currently calls unzGoToNextFile(zipfile) but ignores its return value,
which can leave MiniZip in a stale state; update the block around
entryPath.is_absolute() / entryPath.string().starts_with("..") to capture the
return code from unzGoToNextFile(zipfile), check for an error
(non-UNZ_OK/negative), log an error via g_logger.error with context (filename
and return code) and break/return appropriately (matching the error handling
used in the normal iteration path that checks return values against
global_info.number_entry), so the loop does not continue on failure and the
archive state stays consistent.
src/framework/core/minizip/unzip.c (5)

513-519: ⚠️ Potential issue | 🟠 Major

Zero-initialize the bridge struct in unzOpen2_64().

zopen32_file and zopendisk32_file stay indeterminate here. If a caller omits one of the 64-bit open callbacks, the fallback path jumps through garbage.

🛠️ Suggested fix
     if (pzlib_filefunc_def != NULL)
     {
-        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill = {0};
         zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
-        zlib_filefunc64_32_def_fill.ztell32_file = NULL;
-        zlib_filefunc64_32_def_fill.zseek32_file = NULL;
         return unzOpenInternal(path, &zlib_filefunc64_32_def_fill);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 513 - 519, The bridge struct
zlib_filefunc64_32_def_fill used in unzOpen2_64() is not zero-initialized so
fields like zopen32_file and zopendisk32_file can be indeterminate; fix by
zero-initializing zlib_filefunc64_32_def_fill (or explicitly set zopen32_file
and zopendisk32_file to NULL) before assigning zfile_func64, ztell32_file and
zseek32_file, ensuring any omitted 64-bit callbacks safely fall back instead of
using garbage.

1487-1495: ⚠️ Potential issue | 🟠 Major

Initialize flags before compression_stream_process().

When avail_in != 0, Line 1495 uses flags uninitialized. That is UB on Apple builds.

🛠️ Suggested fix
             compression_status status;
-            compression_stream_flags flags;
+            compression_stream_flags flags = 0;
 
             if (s->pfile_in_zip_read->stream.avail_in == 0)
             {
                 flags = COMPRESSION_STREAM_FINALIZE;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1487 - 1495, The variable
flags used in the call to
compression_stream_process(&s->pfile_in_zip_read->astream, flags) can be
uninitialized when s->pfile_in_zip_read->stream.avail_in != 0; set a sensible
default (e.g., COMPRESSION_STREAM_FINALIZE or COMPRESSION_STREAM_NOT_FINALIZED
depending on intended behavior) when declaring compression_stream_flags flags
before the if, or explicitly assign flags = COMPRESSION_STREAM_NOT_FINALIZED in
the else branch so flags is always initialized prior to calling
compression_stream_process; update the code near the use of compression_status
and compression_stream_flags in unzip.c to ensure flags is always assigned.

1590-1598: ⚠️ Potential issue | 🟠 Major

Advance the local extra-field cursor after each read.

The seek ignores byte_before_the_zipfile, and pos_local_extrafield never advances, so chunked callers on prepended/SFX archives reread the wrong bytes.

🛠️ Suggested fix
     if (ZSEEK64(s->pfile_in_zip_read->z_filefunc, s->pfile_in_zip_read->filestream,
-        s->pfile_in_zip_read->offset_local_extrafield + s->pfile_in_zip_read->pos_local_extrafield,
+        s->pfile_in_zip_read->offset_local_extrafield +
+        s->pfile_in_zip_read->byte_before_the_zipfile +
+        s->pfile_in_zip_read->pos_local_extrafield,
         ZLIB_FILEFUNC_SEEK_SET) != 0)
         return UNZ_ERRNO;
 
     if (ZREAD64(s->pfile_in_zip_read->z_filefunc, s->pfile_in_zip_read->filestream, buf, read_now) != read_now)
         return UNZ_ERRNO;
 
+    s->pfile_in_zip_read->pos_local_extrafield += read_now;
     return (int)read_now;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1590 - 1598, The seek/read
block for reading local extra-field must include the archive prepend offset and
advance the local extra-field cursor: when calling ZSEEK64 use
s->pfile_in_zip_read->byte_before_the_zipfile +
s->pfile_in_zip_read->offset_local_extrafield +
s->pfile_in_zip_read->pos_local_extrafield (not just offset+pos), and after a
successful ZREAD64 increment s->pfile_in_zip_read->pos_local_extrafield by
read_now so subsequent chunked reads continue from the correct position; adjust
the code around ZSEEK64, ZREAD64 and s->pfile_in_zip_read->pos_local_extrafield
accordingly.

1737-1776: ⚠️ Potential issue | 🟠 Major

Restore current_file_ok after an unsuccessful locate.

A miss restores the cursor fields but leaves current_file_ok cleared, so the previously current entry becomes unusable.

🛠️ Suggested fix
     unz_file_info64 cur_file_info_saved;
     unz_file_info64_internal cur_file_info_internal_saved;
     uint64_t num_file_saved = 0;
     uint64_t pos_in_central_dir_saved = 0;
+    uint64_t current_file_ok_saved = 0;
     char current_filename[UNZ_MAXFILENAMEINZIP+1];
     int err = UNZ_OK;
@@
     num_file_saved = s->num_file;
     pos_in_central_dir_saved = s->pos_in_central_dir;
     cur_file_info_saved = s->cur_file_info;
     cur_file_info_internal_saved = s->cur_file_info_internal;
+    current_file_ok_saved = s->current_file_ok;
@@
     s->num_file = num_file_saved;
     s->pos_in_central_dir = pos_in_central_dir_saved;
     s->cur_file_info = cur_file_info_saved;
     s->cur_file_info_internal = cur_file_info_internal_saved;
+    s->current_file_ok = current_file_ok_saved;
     return err;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1737 - 1776, Save the
current_file_ok state at the start (e.g. bool current_file_ok_saved =
s->current_file_ok) alongside
num_file_saved/pos_in_central_dir_saved/cur_file_info_saved/cur_file_info_internal_saved,
and after the failed search restore it (s->current_file_ok =
current_file_ok_saved) before returning err so that the prior current entry
remains usable; locate this change around the unzGoToFirstFile2/unzGoToNextFile2
loop in unzip.c where the saved state is restored.

1915-1970: ⚠️ Potential issue | 🟠 Major

Rebuild stored-entry seeks from the file-data start.

The reset path derives pos_in_zipfile from offset_local_extrafield, which points before the file data, and the rest_read_uncompressed -= (position - total_out_64) math wraps on backward seeks.

🛠️ Safe short-term fix
-    uint64_t stream_pos_begin = 0;
-    uint64_t stream_pos_end = 0;
     uint64_t position = 0;
-    int is_within_buffer = 0;
@@
-    stream_pos_end = s->pfile_in_zip_read->pos_in_zipfile;
-    stream_pos_begin = stream_pos_end;
-
-    if (stream_pos_begin > UNZ_BUFSIZE)
-        stream_pos_begin -= UNZ_BUFSIZE;
-    else
-        stream_pos_begin = 0;
-
-    is_within_buffer = 
-        (s->pfile_in_zip_read->stream.avail_in != 0) &&
-        (s->pfile_in_zip_read->rest_read_compressed != 0 || s->cur_file_info.compressed_size < UNZ_BUFSIZE) &&
-        (position >= stream_pos_begin && position < stream_pos_end);
-
-    if (is_within_buffer)
-    {
-        s->pfile_in_zip_read->stream.next_in += position - s->pfile_in_zip_read->total_out_64;
-        s->pfile_in_zip_read->stream.avail_in = (uInt)(stream_pos_end - position);
-    }
-    else
-    {
-        s->pfile_in_zip_read->stream.avail_in = 0;
-        s->pfile_in_zip_read->stream.next_in = 0;
-
-        s->pfile_in_zip_read->pos_in_zipfile = s->pfile_in_zip_read->offset_local_extrafield + position;
-        s->pfile_in_zip_read->rest_read_compressed = s->cur_file_info.compressed_size - position;
-    }
-
-    s->pfile_in_zip_read->rest_read_uncompressed -= (position - s->pfile_in_zip_read->total_out_64);
+    s->pfile_in_zip_read->stream.avail_in = 0;
+    s->pfile_in_zip_read->stream.next_in = 0;
+    s->pfile_in_zip_read->pos_in_zipfile =
+        s->pfile_in_zip_read->offset_local_extrafield +
+        s->pfile_in_zip_read->size_local_extrafield +
+        position;
+    s->pfile_in_zip_read->rest_read_compressed = s->cur_file_info.compressed_size - position;
+    s->pfile_in_zip_read->rest_read_uncompressed = s->cur_file_info.uncompressed_size - position;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/unzip.c` around lines 1915 - 1970, The seek reset
currently uses offset_local_extrafield directly (which points before the actual
file data) and subtracts unsigned values causing wrap on backward seeks; compute
the actual file-data start first (e.g. file_data_start =
s->pfile_in_zip_read->offset_local_extrafield + size of the local
header/filename/extra fields — use the existing fields that record filename
length and extra-field length such as size_filename / size_extra_field on
pfile_in_zip_read), set s->pfile_in_zip_read->pos_in_zipfile = file_data_start +
position, and update rest_read_uncompressed using a guarded subtraction (only
subtract when position >= s->pfile_in_zip_read->total_out_64 or compute the
delta as a signed value) so total_out_64, stream.total_out and
rest_read_uncompressed remain consistent for forward and backward seeks.
src/framework/core/minizip/ioapi.c (2)

158-180: ⚠️ Potential issue | 🟠 Major

Guard diskFilename before copying into it.

Both spanning helpers dereference diskFilename immediately after malloc(). If allocation fails, they crash instead of returning an open error.

🛠️ Suggested fix
     ioposix = (FILE_IOPOSIX*)stream;
     diskFilename = (char*)malloc(ioposix->filenameLength * sizeof(char));
+    if (diskFilename == NULL)
+        return NULL;
     strncpy(diskFilename, (const char*)ioposix->filename, ioposix->filenameLength);

Apply the same guard in both fopendisk64_file_func() and fopendisk_file_func().

Also applies to: 183-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/ioapi.c` around lines 158 - 180, The function
fopendisk64_file_func currently mallocs diskFilename then immediately uses it;
add a NULL check after the malloc to guard against allocation failure: if
diskFilename is NULL, do not dereference it—return NULL (or the appropriate open
error) immediately. Apply the same fix pattern to fopendisk_file_func; keep the
rest of the logic (strncpy from FILE_IOPOSIX->filename, the filename tweak loop,
and the call to fopen64_file_func) unchanged and ensure free(diskFilename) only
runs when allocation succeeded.

100-117: ⚠️ Potential issue | 🟡 Minor

Close the FILE* when file_build_ioposix() fails.

fopen_file_func() / fopen64_file_func() have already opened the file before calling this helper, so either allocation failure here leaks the descriptor.

🛠️ Suggested fix
 static voidpf file_build_ioposix(FILE *file, const char *filename)
 {
     FILE_IOPOSIX *ioposix = NULL;
     if (file == NULL)
         return NULL;
     /* Fix: legacy minizip missing null checks after malloc (upstream archived) */
     ioposix = (FILE_IOPOSIX*)malloc(sizeof(FILE_IOPOSIX));
-    if (ioposix == NULL)
+    if (ioposix == NULL) {
+        fclose(file);
         return NULL;
+    }
     ioposix->file = file;
     ioposix->filenameLength = (int)strlen(filename) + 1;
     ioposix->filename = (char*)malloc(ioposix->filenameLength * sizeof(char));
     if (ioposix->filename == NULL) {
+        fclose(file);
         free(ioposix);
         return NULL;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/core/minizip/ioapi.c` around lines 100 - 117, When
file_build_ioposix fails after fopen_file_func/fopen64_file_func has already
opened the FILE*, you must close that FILE to avoid leaking the descriptor: call
fclose(file) before returning NULL on allocation failures (after
malloc(sizeof(FILE_IOPOSIX)) fails and after ioposix->filename allocation
fails). Update file_build_ioposix to fclose(file) in those failure paths (keep
freeing ioposix where present) and ensure no further use of file after it is
closed; leave the early return when file == NULL unchanged.
setup_android_deps.sh (1)

137-150: ⚠️ Potential issue | 🟠 Major

Make the zip branch mirror the Python fallback.

zip -r ... init.lua otclientrc.lua aborts when either optional file is absent, and because it updates an existing archive in place it can keep stale entries from previous runs.

🛠️ Suggested fix
 if command -v zip &>/dev/null; then
-    zip -r android/app/src/main/assets/data.zip data mods modules init.lua otclientrc.lua
+    rm -f android/app/src/main/assets/data.zip
+    inputs=(data mods modules)
+    [[ -f init.lua ]] && inputs+=(init.lua)
+    [[ -f otclientrc.lua ]] && inputs+=(otclientrc.lua)
+    zip -r android/app/src/main/assets/data.zip "${inputs[@]}"
 elif command -v python3 &>/dev/null; then

This reproduces the current zip behavior; expect a non-zero exit status when the optional files are absent.

#!/bin/bash
set -euo pipefail

if ! command -v zip >/dev/null; then
  echo "zip is not installed in the sandbox; verify this on a machine with zip available."
  exit 0
fi

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir"/data "$tmpdir"/mods "$tmpdir"/modules

(
  cd "$tmpdir"
  set +e
  zip -r data.zip data mods modules init.lua otclientrc.lua >/tmp/setup_android_deps_zip.log 2>&1
  status=$?
  set -e
  printf 'zip_exit_status=%s\n' "$status"
  cat /tmp/setup_android_deps_zip.log
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setup_android_deps.sh` around lines 137 - 150, The zip branch currently runs
zip -r android/app/src/main/assets/data.zip data mods modules init.lua
otclientrc.lua which fails if the optional files are absent and updates the
archive in-place leaving stale entries; change it to build a fresh archive in a
temporary file or use zip -FS and only pass optional files if they exist.
Concretely, create a tmp file (mktemp), run zip -r "$tmpzip" data mods modules
and then conditionally run zip -u or include init.lua and otclientrc.lua only if
[ -f init.lua ] / [ -f otclientrc.lua ], then move the tmp archive to
android/app/src/main/assets/data.zip (or use zip -FS to remove stale entries)
and clean up the temp file so the behavior mirrors the python fallback and never
errors when optional files are missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/build.gradle.kts`:
- Around line 36-42: The release signing config currently created by
signingConfigs.create("release") points at the debug keystore
(~/.android/debug.keystore) and should be removed or replaced; update the
signingConfigs and buildTypes.release to either (a) load a real release keystore
via Gradle properties or environment variables (e.g., keystoreFile,
keystorePassword, keyAlias, keyPassword) and reference those properties in the
create("release") block, or (b) remove the custom release signing config and use
the existing debug signingConfig for development builds; ensure
buildTypes.release no longer assigns the debug keystore values and instead
references the secure properties or omits the assignment so CI/Docker builds
won’t expect the debug keystore.

In `@src/framework/core/minizip/unzip.c`:
- Around line 831-835: The code checks file_info.disk_num_start against
UINT32_MAX before reading the Zip64 extra-field, but the central-directory field
was read as 16 bits earlier so the Zip64 sentinel is 0xFFFF; change the
conditional to test against UINT16_MAX (0xFFFF) instead of UINT32_MAX so that
unzReadUInt32(&s->z_filefunc, s->filestream_with_CD, &file_info.disk_num_start)
is invoked when the 16-bit placeholder indicates a Zip64 disk start number; keep
the existing error handling (compare return to UNZ_OK and set err = UNZ_ERRNO on
failure).

---

Outside diff comments:
In `@src/framework/platform/androidwindow.cpp`:
- Around line 264-279: The code always sets mouseButton = Fw::MouseLeftButton
and updates m_mouseButtonStates as if left was pressed, then later may remap the
release to Fw::MouseRightButton leaving the left bit stuck; fix by recording the
actually-pressed button at touch-down (e.g. store an originalPressed variable or
set an initialPressed value before potential remap) and use that stored value
when clearing m_mouseButtonStates in the g_dispatcher callback (or
alternatively, when you remap a release to Fw::MouseRightButton synthesize a
matching right-button press event before scheduling the release), updating
m_inputEvent.mouseButton only for the delivered event while always clearing the
bit for the button that was originally set (references: Fw::MouseLeftButton,
Fw::MouseRightButton, m_inputEvent, m_mouseButtonStates, m_lastPress, TOUCH_UP,
m_isDragging, g_dispatcher).

---

Duplicate comments:
In `@build_luajit_android.sh`:
- Around line 22-26: Current probe only compiles with gcc -m32 and can miss
missing 32-bit linker/libs; replace the probe with a full link test
(compile+link a trivial C program) to mirror how HOST_CC="gcc -m32" will be used
when building buildvm so failures are caught early; update the check that
currently runs `gcc -m32 -x c -c /dev/null` to instead attempt compiling and
linking a tiny main() and only install gcc-multilib/g++-multilib if that link
step fails.

In `@setup_android_deps.sh`:
- Around line 137-150: The zip branch currently runs zip -r
android/app/src/main/assets/data.zip data mods modules init.lua otclientrc.lua
which fails if the optional files are absent and updates the archive in-place
leaving stale entries; change it to build a fresh archive in a temporary file or
use zip -FS and only pass optional files if they exist. Concretely, create a tmp
file (mktemp), run zip -r "$tmpzip" data mods modules and then conditionally run
zip -u or include init.lua and otclientrc.lua only if [ -f init.lua ] / [ -f
otclientrc.lua ], then move the tmp archive to
android/app/src/main/assets/data.zip (or use zip -FS to remove stale entries)
and clean up the temp file so the behavior mirrors the python fallback and never
errors when optional files are missing.

In `@src/framework/core/minizip/ioapi.c`:
- Around line 158-180: The function fopendisk64_file_func currently mallocs
diskFilename then immediately uses it; add a NULL check after the malloc to
guard against allocation failure: if diskFilename is NULL, do not dereference
it—return NULL (or the appropriate open error) immediately. Apply the same fix
pattern to fopendisk_file_func; keep the rest of the logic (strncpy from
FILE_IOPOSIX->filename, the filename tweak loop, and the call to
fopen64_file_func) unchanged and ensure free(diskFilename) only runs when
allocation succeeded.
- Around line 100-117: When file_build_ioposix fails after
fopen_file_func/fopen64_file_func has already opened the FILE*, you must close
that FILE to avoid leaking the descriptor: call fclose(file) before returning
NULL on allocation failures (after malloc(sizeof(FILE_IOPOSIX)) fails and after
ioposix->filename allocation fails). Update file_build_ioposix to fclose(file)
in those failure paths (keep freeing ioposix where present) and ensure no
further use of file after it is closed; leave the early return when file == NULL
unchanged.

In `@src/framework/core/minizip/unzip.c`:
- Around line 513-519: The bridge struct zlib_filefunc64_32_def_fill used in
unzOpen2_64() is not zero-initialized so fields like zopen32_file and
zopendisk32_file can be indeterminate; fix by zero-initializing
zlib_filefunc64_32_def_fill (or explicitly set zopen32_file and zopendisk32_file
to NULL) before assigning zfile_func64, ztell32_file and zseek32_file, ensuring
any omitted 64-bit callbacks safely fall back instead of using garbage.
- Around line 1487-1495: The variable flags used in the call to
compression_stream_process(&s->pfile_in_zip_read->astream, flags) can be
uninitialized when s->pfile_in_zip_read->stream.avail_in != 0; set a sensible
default (e.g., COMPRESSION_STREAM_FINALIZE or COMPRESSION_STREAM_NOT_FINALIZED
depending on intended behavior) when declaring compression_stream_flags flags
before the if, or explicitly assign flags = COMPRESSION_STREAM_NOT_FINALIZED in
the else branch so flags is always initialized prior to calling
compression_stream_process; update the code near the use of compression_status
and compression_stream_flags in unzip.c to ensure flags is always assigned.
- Around line 1590-1598: The seek/read block for reading local extra-field must
include the archive prepend offset and advance the local extra-field cursor:
when calling ZSEEK64 use s->pfile_in_zip_read->byte_before_the_zipfile +
s->pfile_in_zip_read->offset_local_extrafield +
s->pfile_in_zip_read->pos_local_extrafield (not just offset+pos), and after a
successful ZREAD64 increment s->pfile_in_zip_read->pos_local_extrafield by
read_now so subsequent chunked reads continue from the correct position; adjust
the code around ZSEEK64, ZREAD64 and s->pfile_in_zip_read->pos_local_extrafield
accordingly.
- Around line 1737-1776: Save the current_file_ok state at the start (e.g. bool
current_file_ok_saved = s->current_file_ok) alongside
num_file_saved/pos_in_central_dir_saved/cur_file_info_saved/cur_file_info_internal_saved,
and after the failed search restore it (s->current_file_ok =
current_file_ok_saved) before returning err so that the prior current entry
remains usable; locate this change around the unzGoToFirstFile2/unzGoToNextFile2
loop in unzip.c where the saved state is restored.
- Around line 1915-1970: The seek reset currently uses offset_local_extrafield
directly (which points before the actual file data) and subtracts unsigned
values causing wrap on backward seeks; compute the actual file-data start first
(e.g. file_data_start = s->pfile_in_zip_read->offset_local_extrafield + size of
the local header/filename/extra fields — use the existing fields that record
filename length and extra-field length such as size_filename / size_extra_field
on pfile_in_zip_read), set s->pfile_in_zip_read->pos_in_zipfile =
file_data_start + position, and update rest_read_uncompressed using a guarded
subtraction (only subtract when position >= s->pfile_in_zip_read->total_out_64
or compute the delta as a signed value) so total_out_64, stream.total_out and
rest_read_uncompressed remain consistent for forward and backward seeks.

In `@src/framework/core/unzipper.cpp`:
- Around line 87-91: The unsafe-entry branch currently calls
unzGoToNextFile(zipfile) but ignores its return value, which can leave MiniZip
in a stale state; update the block around entryPath.is_absolute() /
entryPath.string().starts_with("..") to capture the return code from
unzGoToNextFile(zipfile), check for an error (non-UNZ_OK/negative), log an error
via g_logger.error with context (filename and return code) and break/return
appropriately (matching the error handling used in the normal iteration path
that checks return values against global_info.number_entry), so the loop does
not continue on failure and the archive state stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4cc04b4f-d77f-43ff-9a40-879f6f2c8f86

📥 Commits

Reviewing files that changed from the base of the PR and between dc7c9fb and 6c174e6.

📒 Files selected for processing (23)
  • .dockerignore
  • .gitignore
  • Dockerfile.android
  • android/BUILDING.md
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • build_luajit_android.sh
  • setup_android_deps.sh
  • setup_android_wsl.sh
  • src/CMakeLists.txt
  • src/framework/core/minizip/crypt.c
  • src/framework/core/minizip/crypt.h
  • src/framework/core/minizip/ioapi.c
  • src/framework/core/minizip/ioapi.h
  • src/framework/core/minizip/ioapi_mem.c
  • src/framework/core/minizip/ioapi_mem.h
  • src/framework/core/minizip/unzip.c
  • src/framework/core/minizip/unzip.h
  • src/framework/core/unzipper.cpp
  • src/framework/platform/androidmanager.cpp
  • src/framework/platform/androidwindow.cpp
  • src/framework/ui/uitextedit.cpp
  • src/main.cpp
✅ Files skipped from review due to trivial changes (7)
  • .gitignore
  • android/app/src/main/AndroidManifest.xml
  • .dockerignore
  • src/framework/ui/uitextedit.cpp
  • setup_android_wsl.sh
  • src/framework/core/minizip/crypt.h
  • android/BUILDING.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/framework/platform/androidmanager.cpp
  • src/main.cpp

@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch from 6c174e6 to 097510c Compare March 26, 2026 16:30
…aJIT

Fixes opentibiabr#1642

Black screen fixes:
- Add null check for AAssetManager_open (data.zip missing crash)
- Fix unzipper: create_directory -> create_directories for nested paths
- Fix APP_CMD_LOW_MEMORY permanently destroying EGL surface (no recovery)
- Fix keyboard auto-opening on programmatic focus (only on user tap)

Build modernization:
- Replace pre-compiled Google Drive archive with self-contained build
- LuaJIT cross-compiled via build_luajit_android.sh for all 4 ABIs
  (arm64-v8a, armeabi-v7a, x86_64, x86)
- All other deps (OpenAL, Freetype, protobuf, etc.) via vcpkg
- Vendored minizip source (nmoinvaz fork)
- Separate Android GLES3 from WASM GLES2

LuaJIT cross-compilation requires HOST_CC="gcc -m32" for 32-bit targets
and -DLUAJIT_UNWIND_EXTERNAL for Android NDK compatibility.
See android/BUILDING.md for full details and references to LuaJIT issues
opentibiabr#440, opentibiabr#477, opentibiabr#664.

Build infrastructure:
- build_luajit_android.sh: cross-compile LuaJIT for all ABIs
- setup_android_deps.sh: complete pre-build automation
- Dockerfile.android: reproducible Docker build
- android/BUILDING.md: step-by-step build guide with troubleshooting
- ALOGD debug logging in main.cpp
- Release signing config in build.gradle.kts
@Kizuno18 Kizuno18 force-pushed the fix/android-black-screen branch from 097510c to 29c1e12 Compare March 26, 2026 17:59
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android build

1 participant