diff --git a/.gitattributes b/.gitattributes index 60957e6..605224a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,14 +2,23 @@ *.c text eol=lf *.cc text eol=lf *.cmake text eol=lf +*.gradle text eol=lf *.h text eol=lf -*.md text eol=lf +*.java text eol=lf *.json text eol=lf +*.md text eol=lf *.plist text eol=lf +*.pro text eol=lf +*.properties text eol=lf +*.xml text eol=lf *.yml text eol=lf .clang-format text eol=lf .editorconfig text eol=lf .gitattributes text eol=lf .gitignore text eol=lf +gradlew text eol=lf CMakeLists.txt text eol=lf LICENSE text eol=lf + +# Force CRLF +*.bat text eol=crlf diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index f12d1c5..aefe72a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -38,6 +38,49 @@ jobs: - name: cppcheck run: cppcheck --std=c++17 src/ + android: + name: Android + + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + cache: gradle + + - name: Cache cmake build + uses: actions/cache@v3 + with: + path: os/android/app/.cxx + key: android-cmake-v1 + + - name: Setup signing config + run: | + cd os/android + echo "$KEYSTORE_FILE_BASE64" | base64 --decode > debug.keystore + echo "$KEYSTORE_PROPERTIES_FILE_BASE64" | base64 --decode > debug-keystore.properties + env: + KEYSTORE_FILE_BASE64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_FILE_BASE64 }} + KEYSTORE_PROPERTIES_FILE_BASE64: ${{ secrets.ANDROID_DEBUG_KEYSTORE_PROPERTIES_FILE_BASE64 }} + + - name: Build + run: | + cd os/android + ./gradlew assembleDebug + + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: fallout2-ce-debug.apk + path: os/android/app/build/outputs/apk/debug/app-debug.apk + retention-days: 7 + linux: name: Linux (${{ matrix.arch }}) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3668281..6e28054 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,51 @@ defaults: shell: bash jobs: + android: + name: Android + + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + cache: gradle + + - name: Cache cmake build + uses: actions/cache@v3 + with: + path: os/android/app/.cxx + key: android-cmake-v1 + + - name: Setup signing config + run: | + cd os/android + echo "$KEYSTORE_FILE_BASE64" | base64 --decode > release.keystore + echo "$KEYSTORE_PROPERTIES_FILE_BASE64" | base64 --decode > release-keystore.properties + env: + KEYSTORE_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_FILE_BASE64 }} + KEYSTORE_PROPERTIES_FILE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PROPERTIES_FILE_BASE64 }} + + - name: Build + run: | + cd os/android + ./gradlew assembleRelease + + - name: Upload + run: | + cd os/android/app/build/outputs/apk/release + cp app-release.apk fallout2-ce-android.apk + gh release upload ${{ github.ref_name }} fallout2-ce-android.apk + rm fallout2-ce-android.apk + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + linux: name: Linux (${{ matrix.arch }}) diff --git a/README.md b/README.md index 4f10a38..beaaf8a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,20 @@ $ sudo apt install libsdl2-2.0-0 - Run `fallout2-ce.app`. +### Android + +> **NOTE**: Fallout 2 was designed with mouse in mind. There are many controls that require precise cursor positioning, which is not possible with fingers. When playing on Android you'll use fingers to move mouse cursor, not a character, or a map. Double tap to "click" left mouse button in the current cursor position, triple tap to "click" right mouse button. It might feel awkward at first, but it's super handy - you can play with just a thumb. This is not set in stone and might change in the future. + +- Download `fallout2-ce.apk` and copy it to your device. Open it with file explorer, follow instructions (install from unknown source). + +- Run the game once, it will say `Couldn't find/load text fonts` and create a folder for data assets. + +- Open file explorer, navigate to `Android/data/com.alexbatalov.fallout2ce/files`, delete junk folders inside (they will be named as game files, just delete them). + +- Copy `master.dat`, `critter.dat`, `patch000.dat` and `data` folder from your Windows/macOS installation to the folder above. + +- Run the game again. + ## Contributing Integrating Sfall goodies is the top priority. Quality of life updates are OK too. Please no large scale refactorings at this time as we need to reconcile changes from Reference Edition, which will make this process slow and error-prone. In any case open up an issue with your suggestion or to notify other people that something is being worked on. diff --git a/os/android/.gitignore b/os/android/.gitignore index cf7a227..f0f2a29 100644 --- a/os/android/.gitignore +++ b/os/android/.gitignore @@ -7,3 +7,7 @@ /.idea/navEditor.xml /.idea/workspace.xml /local.properties +/debug-keystore.properties +/debug.keystore +/release-keystore.properties +/release.keystore diff --git a/os/android/app/.gitignore b/os/android/app/.gitignore index 8268dac..3a72371 100644 --- a/os/android/app/.gitignore +++ b/os/android/app/.gitignore @@ -1,2 +1,5 @@ /.cxx /build + +# TODO: Cleanup root .gitignore +!/src/debug diff --git a/os/android/app/build.gradle b/os/android/app/build.gradle index 633ebdf..e5f4ff0 100644 --- a/os/android/app/build.gradle +++ b/os/android/app/build.gradle @@ -26,10 +26,49 @@ android { } } + signingConfigs { + // Override default debug signing config to make sure every CI runner + // uses the same key for painless updates. + def debugKeystorePropertiesFile = rootProject.file('debug-keystore.properties') + if (debugKeystorePropertiesFile.exists()) { + def debugKeystoreProperties = new Properties() + debugKeystoreProperties.load(new FileInputStream(debugKeystorePropertiesFile)) + + debug { + storeFile rootProject.file(debugKeystoreProperties.getProperty('storeFile')) + storePassword debugKeystoreProperties.getProperty('storePassword') + keyAlias debugKeystoreProperties.getProperty('keyAlias') + keyPassword debugKeystoreProperties.getProperty('keyPassword') + } + } + + def releaseKeystoreProperties = new Properties() + def releaseKeystorePropertiesFile = rootProject.file('release-keystore.properties') + if (releaseKeystorePropertiesFile.exists()) { + releaseKeystoreProperties.load(new FileInputStream(releaseKeystorePropertiesFile)) + + release { + storeFile rootProject.file(releaseKeystoreProperties.getProperty('storeFile')) + storePassword releaseKeystoreProperties.getProperty('storePassword') + keyAlias releaseKeystoreProperties.getProperty('keyAlias') + keyPassword releaseKeystoreProperties.getProperty('keyPassword') + } + } + } + buildTypes { + debug { + // Prevents signing keys clashes between debug and release versions + // for painless development. + applicationIdSuffix '.debug' + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + + // Release signing config is optional and might not be present in CI + // builds, hence `findByName`. + signingConfig signingConfigs.findByName('release') } } diff --git a/os/android/app/src/debug/res/values/strings.xml b/os/android/app/src/debug/res/values/strings.xml new file mode 100644 index 0000000..80ed3dc --- /dev/null +++ b/os/android/app/src/debug/res/values/strings.xml @@ -0,0 +1,3 @@ + + Fallout 2 (Debug) + diff --git a/os/android/app/src/main/res/values/strings.xml b/os/android/app/src/main/res/values/strings.xml index 6ea845c..03e515a 100644 --- a/os/android/app/src/main/res/values/strings.xml +++ b/os/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Fallout 2 Community Edition + Fallout 2 diff --git a/src/core.cc b/src/core.cc index 274c141..284d34f 100644 --- a/src/core.cc +++ b/src/core.cc @@ -1272,6 +1272,11 @@ void _GNW95_process_message() // The data is accumulated in SDL itself and will be processed // in `_mouse_info`. break; + case SDL_FINGERDOWN: + case SDL_FINGERMOTION: + case SDL_FINGERUP: + handleTouchFingerEvent(&(e.tfinger)); + break; case SDL_KEYDOWN: case SDL_KEYUP: if (!keyboardIsDisabled()) { diff --git a/src/dinput.cc b/src/dinput.cc index b72987b..5272ccc 100644 --- a/src/dinput.cc +++ b/src/dinput.cc @@ -1,6 +1,18 @@ #include "dinput.h" -#include +static int gTouchMouseLastX = 0; +static int gTouchMouseLastY = 0; +static int gTouchMouseDeltaX = 0; +static int gTouchMouseDeltaY = 0; + +static int gTouchFingers = 0; +static unsigned int gTouchGestureLastTouchDownTimestamp = 0; +static unsigned int gTouchGestureLastTouchUpTimestamp = 0; +static int gTouchGestureTaps = 0; +static bool gTouchGestureHandled = false; + +extern int screenGetWidth(); +extern int screenGetHeight(); // 0x4E0400 bool directInputInit() @@ -47,9 +59,42 @@ bool mouseDeviceUnacquire() // 0x4E053C bool mouseDeviceGetData(MouseData* mouseState) { +#if __ANDROID__ + mouseState->x = gTouchMouseDeltaX; + mouseState->y = gTouchMouseDeltaY; + mouseState->buttons[0] = 0; + mouseState->buttons[1] = 0; + gTouchMouseDeltaX = 0; + gTouchMouseDeltaY = 0; + + if (gTouchFingers == 0) { + if (SDL_GetTicks() - gTouchGestureLastTouchUpTimestamp > 150) { + if (!gTouchGestureHandled) { + if (gTouchGestureTaps == 2) { + mouseState->buttons[0] = 1; + gTouchGestureHandled = true; + } else if (gTouchGestureTaps == 3) { + mouseState->buttons[1] = 1; + gTouchGestureHandled = true; + } + } + } + } else if (gTouchFingers == 1) { + if (SDL_GetTicks() - gTouchGestureLastTouchDownTimestamp > 150) { + if (gTouchGestureTaps == 1) { + mouseState->buttons[0] = 1; + gTouchGestureHandled = true; + } else if (gTouchGestureTaps == 2) { + mouseState->buttons[1] = 1; + gTouchGestureHandled = true; + } + } + } +#else Uint32 buttons = SDL_GetRelativeMouseState(&(mouseState->x), &(mouseState->y)); mouseState->buttons[0] = (buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0; mouseState->buttons[1] = (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0; +#endif return true; } @@ -100,3 +145,44 @@ bool keyboardDeviceInit() void keyboardDeviceFree() { } + +void handleTouchFingerEvent(SDL_TouchFingerEvent* event) +{ + int windowWidth = screenGetWidth(); + int windowHeight = screenGetHeight(); + + if (event->type == SDL_FINGERDOWN) { + gTouchFingers++; + + gTouchMouseLastX = (int)(event->x * windowWidth); + gTouchMouseLastY = (int)(event->y * windowHeight); + gTouchMouseDeltaX = 0; + gTouchMouseDeltaY = 0; + + if (event->timestamp - gTouchGestureLastTouchDownTimestamp > 250) { + gTouchGestureTaps = 0; + gTouchGestureHandled = false; + } + + gTouchGestureLastTouchDownTimestamp = event->timestamp; + } else if (event->type == SDL_FINGERMOTION) { + int prevX = gTouchMouseLastX; + int prevY = gTouchMouseLastY; + gTouchMouseLastX = (int)(event->x * windowWidth); + gTouchMouseLastY = (int)(event->y * windowHeight); + gTouchMouseDeltaX += gTouchMouseLastX - prevX; + gTouchMouseDeltaY += gTouchMouseLastY - prevY; + } else if (event->type == SDL_FINGERUP) { + gTouchFingers--; + + int prevX = gTouchMouseLastX; + int prevY = gTouchMouseLastY; + gTouchMouseLastX = (int)(event->x * windowWidth); + gTouchMouseLastY = (int)(event->y * windowHeight); + gTouchMouseDeltaX += gTouchMouseLastX - prevX; + gTouchMouseDeltaY += gTouchMouseLastY - prevY; + + gTouchGestureTaps++; + gTouchGestureLastTouchUpTimestamp = event->timestamp; + } +} diff --git a/src/dinput.h b/src/dinput.h index 113e998..7cb8144 100644 --- a/src/dinput.h +++ b/src/dinput.h @@ -1,6 +1,8 @@ #ifndef DINPUT_H #define DINPUT_H +#include + typedef struct MouseData { int x; int y; @@ -26,4 +28,6 @@ void mouseDeviceFree(); bool keyboardDeviceInit(); void keyboardDeviceFree(); +void handleTouchFingerEvent(SDL_TouchFingerEvent* event); + #endif /* DINPUT_H */ diff --git a/src/win32.cc b/src/win32.cc index 70c6747..8b4ab30 100644 --- a/src/win32.cc +++ b/src/win32.cc @@ -40,6 +40,8 @@ int main(int argc, char* argv[]) #endif #if __ANDROID__ + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); chdir(SDL_AndroidGetExternalStoragePath()); #endif