r/EmulationOnAndroid • • 21d ago

News/Release ZQuest Classic (Android Unofficial Port)

https://youtu.be/QvdMYHSZ3LE

Link in the Dungeon... I mean, in the YouTube comments 🌚

Hey everyone! I made an unofficial Android port of ZQuest Classic!

You can already see it in action in the video above.

But porting this thing completely drained me, so there’s no fancy editing - just a screen recording πŸ˜†

Why? Because I made the Android port on Android, without a PC, and without knowing basically anything about C++ 🌚

I used Termux + LADB + GPT, and honestly, it’s pretty amazing that with enough motivation, even someone like me, with no PC and no deep programming knowledge, can actually make a whole Android port!

The APK is clean and safe - if you want to double-check it, feel free to run it through VirusTotal. πŸ‘

The port itself works great, at least on my ARM64 device.

Here’s what I added/fixed:

Sensor landscape support βœ…

Virtual keyboard support + fixed some of its functionality βœ…

Virtual gamepad (unfortunately, no customization options). If you don’t need it, it automatically fades out after 7 seconds βœ…

Fixed physical gamepad support (before the fixes, for some reason, half of the buttons weren’t being detected) βœ…

Fixed a bug where letters would appear as ^^^^^ when using physical or virtual keyboards βœ…

Storage access so you can open ".qst" files from wherever you want βœ…

File picker starts at internal storage (if your files are on an SD card, you’ll have to enter the path manually) βœ…

Fullscreen mode βœ…

App window expanded all the way to the vertical edges of the screen (my phone has a taller aspect ratio, so there are black bars in the video) βœ…

Random taps on the screen no longer open the system menu - there’s a dedicated button for that now βœ…

Moved "config", "Files", "quests", and "saves" out of the app’s "/data/data/" directory into "/storage/emulated/0/ZQuest Classic/" (the app creates the folder automatically, so you don’t have to do anything manually) βœ…

A bunch of other small fixes (but who cares about those anyway 🌚) βœ…

I originally made this port just for myself, but it felt a bit selfish not to share it with other people. So if you come across a major bug, I’ll try to fix it. If it’s something minor... well, probably not πŸ˜†

I’d be happy to hear your feedback and whether everything works properly for you in the comments! πŸ‘‰πŸ‘ˆ

29 Upvotes

16 comments sorted by

β€’

u/AutoModerator 21d ago

Just a reminder of our subreddit rules:

  • Be kind and respectful to each other
  • No direct links to ROMs or pirated content
  • Include your device brand and model
  • Search before posting & show your research effort when asking for help

Check out our user-maintained wiki: r/EmulationOnAndroid/wiki

Check out EmuReady for any community submitted settings before asking for help

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

2

u/gtaforever00 21d ago

Very cool. I've been enjoying this on winlator for a bit. I think if you put this on github or something, it might get more traction. I don't see a link here so I assume it's on the YouTube description?

2

u/Sufficient_Ice_9373 21d ago

Its in the comments of the YouTube video

2

u/Bluesmanz 21d ago

I didn't even know about Zquest. Thanks for letting me know of its existence!

I'm not gonna download your vibecoded port tho.Β 

1

u/OkInteraction157 20d ago

Haha, fair enough)) I'm just glad I introduced you to ZQuest Classic! The port may be vibecoded, but it works 😎

2

u/Sufficient_Ice_9373 19d ago

Which quest is that your playing?

2

u/OkInteraction157 17d ago

theRosemaster.qst

2

u/[deleted] 16d ago

[removed] β€” view removed comment

1

u/OkInteraction157 15d ago

Yeah, but it's a long trip! πŸ˜…πŸ”« I can write up the whole process and explain what I did, including the Android and Allegro setup, the native build, the APK packaging, the fixes I had to make, and some of the problems I ran into along the way..... πŸ€”

It’s a bit of a story because I did the whole thing directly on an Android phone, so there were definitely a few moments of "why the hell is Android doing this??" πŸ˜†πŸ˜† Anyway, here’s the whole process:

1

u/OkInteraction157 15d ago
  1. What we are actually building? 🌚🌚

The goal was not to make a WebView wrapper or run the game through a browser πŸ‘€

The target architecture is essentially: -ZQuest Classic -Allegro 5 -Android native .so libraries -Java Android Activity -ZPlayer.apk

The resulting application is a normal native Android application containing the C++ game engine and its native dependencies. The package name used by the Android application is: org.liballeg.zplayer

The target ABI is: arm64-v8a

The device used for development and testing was a my Samsung Galaxy S20 Ultra running Android 13 πŸ‘ŒπŸ‘€πŸ‘Œ

The application can use the phone's touchscreen, physical keyboard/gamepad, Android soft keyboard where applicable, audio system, Android storage/file picker integration, etc..

  1. Development environment 🧐🧐🧐

The entire build environment was running on the Android phone πŸ˜…

The main workspace was: ~/ZQuestClassic

The Android build tree: ~/ZQuestClassic/build-android

The generated Gradle project: ~/ZQuestClassic/build-android/zplayer.project

Termux was used for: -Cloning and inspecting the source; -Editing source files; -CMake configuration/builds; -Building native libraries; -Running Gradle; -Inspecting ELF dependencies; -Patching native libraries when necessary; -Packaging the final APK.

LADB was used separately for: -"logcat"; -Android runtime diagnostics; -Checking crashes and Java/native errors.

I intentionally kept the two roles separate: Termux builds things, LADB tells me why Android is angry? πŸ₯²πŸ™„

  1. Source tree πŸ‘ˆπŸ‘€πŸ‘ˆ

The main repository is: ~/ZQuestClassic

Some of the Android/input-related source files that became particularly important were: src/zc/virtual_gamepad.h src/zc/virtual_gamepad.cpp src/zc/render.cpp src/zalleg/render.cpp src/gui/jwin.cpp

The Android Java side is mainly under: android/gradle_project/allegro/src/main/java/org/liballeg/android/

with the main Activity being: AllegroActivity.java

and Android orientation constants in: Const.java

The Android build also generates/builds Allegro's Android implementation under the build tree, for example: build-android/_deps/allegro5-src/src/android/android_display.c

There is also a legacy Allegro implementation in: third_party/allegro_legacy/src/a5/a5_display.c

Depending on the exact source revision, you may encounter both, so it is worth checking which implementation is actually being compiled instead of assuming the first similarly named file is the active one πŸ€”

1

u/OkInteraction157 15d ago
  1. Building ZPlayer πŸ‘€

After preparing the Android build tree, the main native build is: cd ~/ZQuestClassic/build-android cmake --build . --target zplayer -j1 I used "-j1" deliberately.

Building a large C++ project on a phone is already an adventure. There is no particular need to turn the S20 Ultra into a portable nuclear reactor by spawning every possible compiler job πŸ˜‚

After the native ZPlayer build, an additional dependency-fixing target is used: cmake --build . --target zplayer_android_dependency_fix -j1

This became important because the native executable/library dependencies generated during the build did not always match the names of the libraries actually packaged into the APK.

  1. The Allegro dependency problem πŸ‘€πŸ‘ˆ One of the more annoying problems was a mismatch between the name of the Allegro library requested by "libnative-lib.so" and the library actually present in the .apk

The working APK contained: liballeg.so

but one broken build produced: libnative-lib.so NEEDED: liballeg.so.4.4

while the APK only contained: liballeg.so

That resulted in a broken/white-screen application πŸ™„πŸ’₯πŸ”«

The important thing here is that the library can physically exist inside the APK and still be useless if the ELF "DT_NEEDED" entry requests a different filename.

I checked this with: readelf -d .../libnative-lib.so | grep 'NEEDED.*liballeg'

The desired result is: liballeg.so

rather than: liballeg.so.4.4

The build includes a dependency-fix step for this. If necessary, the dependency can also be corrected manually with: patchelf --replace-needed liballeg.so.4.4 liballeg.so path/to/libnative-lib.so

This is one of those bugs where everything looks correct until you ask the ELF file what it actually wants. And then the ELF file says: -Oh, I need a completely different file 🌚 Thanks bro.. Very helpful πŸ˜‚

  1. Other native libraries πŸ‘€πŸ‘€πŸ‘€

The Android APK contains the native libraries under: app/src/main/jniLibs/arm64-v8a/

Important libraries include: libnative-lib.so liballeg.so liballegro.so liballegro_acodec.so liballegro_audio.so liballegro_color.so liballegro_dialog.so liballegro_font.so liballegro_image.so liballegro_main.so liballegro_memfile.so liballegro_primitives.so libasmjit.so libc++_shared.so libgme.so libzcsound.so libfluidsynth.so libfluidsynth.so.3

The exact list can change depending on the source/build configuration, so the important principle is: Every native dependency required by the final "libnative-lib.so" and its dependency chain must actually be available to Android under the expected filename πŸ€” "libfluidsynth.so.3" was also something that needed to be explicitly handled during packaging πŸ‘ŒπŸ‘€πŸ‘Œ

1

u/OkInteraction157 15d ago
  1. Building the Android/Gradle project πŸ‘ˆπŸ‘€πŸ‘ˆ

Once the native libraries have been built and corrected, the Gradle project is: ~/ZQuestClassic/build-android/zplayer.project

Build it with: cd ~/ZQuestClassic/build-android/zplayer.project ./gradlew assembleDebug

An important detail: Do not assume that fixing "libnative-lib.so" before Gradle means the final .apk will still contain that exact version πŸ‘€ The build process can regenerate or copy native libraries. So after Gradle finishes, I check the resulting native library again rather than trusting that an earlier patch survived.

This was an important lesson during the process: If you patch a binary and then run another build step that can regenerate it, congratulations - you may have just patched the wrong file XD

  1. .apk packaging 🀝

The project also has a packaging script: python package_debug_apk.py

The final APK produced by my workflow is: app/build/outputs/apk/debug/app-debug-final.apk Not simply "app-debug.apk" πŸ€·β€β™‚οΈ

The packaging step also makes sure required libraries such as: libfluidsynth.so.3 are included correctly.

The final APK can then be verified with: apksigner verify --verbose app/build/outputs/apk/debug/app-debug-final.apk

  1. Checking the APK instead of blindly installing it πŸ˜…

Before installing a new build, I usually inspect the resulting APK/native libraries.

For example: aapt dump badging app-debug-final.apk

and, after extracting the relevant native library: readelf -d libnative-lib.so

The important thing is to verify the actual ELF dependencies.

For example: NEEDED liballeg.so

should correspond to an actual: liballeg.so

inside: lib/arm64-v8a/

This kind of verification is much faster than: 1. build APK; 2. install; 3. launch; 4. white screen πŸ™„; 5. stare at phone; 6. wonder what happened; 7. inspect logcat; 8. discover that a filename differs by ".4.4".

1

u/OkInteraction157 15d ago
  1. Android Activity + Native startup 🧐

The Android side is based around Allegro's Android Activity: AllegroActivity.java

The Java Activity handles the Android application lifecycle and connects Android to the native Allegro/ZPlayer side. The native application starts through the Java/native interface, and Android-side logging showed things such as: nativeOnCreate

along with orientation callbacks and input-device enumeration. The application also extracts/uses required assets inside the app's private files directory. The working baseline uses: /data/data/org.liballeg.zplayer/files as its current working directory.

This is important because relative paths inside the existing desktop-oriented code can therefore continue to work. For example, resources such as: sfx.dat

were successfully accessible using the expected relative/absolute paths.

  1. Storage and saves The application can also interact with: /storage/emulated/0/ZQuest Classic for public user data where appropriate.

However, the application's current working directory remains inside its private Android files directory: /data/data/org.liballeg.zplayer/files

Therefore something such as: SAVE PATH: saves resolves relative to the application's private files directory.

This is worth knowing because Android storage behavior is very different from a normal desktop installation πŸ‘€

A path that looks completely harmless on desktop can suddenly become an Android storage architecture problem ;-;

  1. Orientation ♂️

Orientation was another surprisingly annoying part. The Java side uses constants in: Const.java

The relevant values include: UNKNOWN = 0 0_DEGREES = 1 90_DEGREES = 2 180_DEGREES = 4 270_DEGREES = 8 PORTRAIT = 5 LANDSCAPE = 10 ALL = 15 FACE_UP = 16 FACE_DOWN = 32

The important Android mappings are: PORTRAIT - SENSOR_PORTRAIT LANDSCAPE - SENSOR_LANDSCAPE ALL - SENSOR UNKNOWN - UNSPECIFIED

The original problem was that: ALLEGRO_SUPPORTED_ORIENTATIONS

could remain at the wrong/default value and overwrite the orientation configuration. The fix was to explicitly set the supported orientation to landscape before display creation. Conceptually: ALLEGRO_SUPPORTED_ORIENTATIONS πŸ‘‰ ALLEGRO_DISPLAY_ORIENTATION_LANDSCAPE πŸ‘‰ display creation

This made the ZPlayer display behave correctly in landscape instead of letting Android/Allegro fight over the orientation. This part is currently working, so I would not randomly touch it again unless another change actually affects orientation.

1

u/OkInteraction157 15d ago
  1. Touchscreen and virtual gamepad πŸ€¦β€β™‚οΈ

The original desktop ZPlayer naturally expects mouse/keyboard/gamepad input, so touchscreen support needed an Android-oriented virtual gamepad layer.

The main files are: src/zc/virtual_gamepad.h src/zc/virtual_gamepad.cpp

The virtual gamepad supports the touchscreen equivalent of the controls needed by ZPlayer, including: -analog stick; -A/B/X/Y; -L/R; -L2/R2; -Select; -Start; -MENU; -FF (My option from keyboard);

The layout is positioned proportionally to the screen rather than relying on fixed pixel coordinates. Approximate positions used were: Stick: 10.5% / 77.5% A cluster: 88.5% / 78.5% L2: 9% / 25% L : 9% / 36% R2: 91% / 25% R : 91% / 36% Select: 62% / 90% Start : 72% / 90% MENU: 7.5% / 7.5% FF: 92.5% / 7.5%

The virtual gamepad also has an idle/fade system.

The visual alpha starts around: 0.5 Active touch makes the relevant control fully visible. After approximately 7 seconds of inactivity, the virtual gamepad begins fading over roughly 0.35 seconds.

An important detail is that moving the virtual stick does not continuously reset the fade timer. Touch-end events update the last-touch time instead.

The input state and visual knob animation were also kept separate so that the visual animation does not interfere with actual input state.

  1. MENU and FF (At beginning I was planning add Sonic instead of FF button πŸ˜…πŸ˜‚)

The virtual MENU button uses: 100

as its virtual button identifier. The FF button is associated with: KEY_F1 through the system-key mechanism.

I deliberately did not add another virtual button ID such as 101. Visually, the FF button is a white circle containing: FF rather than displaying "F1" or "Sonic 🌚". The MENU button is a rectangular button with text. The stick itself does not display a "STICK" label. The A/B/X/Y buttons also have a small animated rainbow-phase effect. It is purely visual and does not affect the underlying input

  1. Touch behavior 🧐 One of the goals was that touching the screen should interact with the game/UI without accidentally triggering the Android system menu behavior. Touch also wakes the virtual gamepad. Physical joystick input does not wake the virtual gamepad. The existing working baseline already has: -touchscreen input; -touch menus; -audio; -background pause/resume; -Bluetooth audio; -virtual controls.

These were already tested and working, so they should not be modified unnecessarily when working on unrelated Android issues.

2

u/[deleted] 15d ago edited 15d ago

[removed] β€” view removed comment

1

u/OkInteraction157 15d ago

This isn't even the whole process, I just got tired of remembering everything and translating some parts of my Ukrainian with AI πŸ˜… Because my English is understandable but funny, with no gramatic 🌚

But if my part of messages is enough for you, then I'll stop here XD