diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 9125716af..0052d383d 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -9,28 +9,18 @@ on: jobs: build: - - runs-on: ubuntu-latest + # Runner GAMING labels: self-hosted, Windows, X64 + runs-on: [self-hosted, Windows, X64] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'true' - - name: set up JDK 18 - uses: actions/setup-java@v3 + - name: set up JDK 17 + uses: actions/setup-java@v5 with: - java-version: '18' + java-version: '17' distribution: 'temurin' - cache: gradle - - name: Decode Google Services Info - uses: timheuer/base64-to-file@v1.2 - with: - fileName: 'google-services.json' - fileDir: '.' - encodedString: ${{ secrets.GOOGLE_SERVICES_JSON }} - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build App bundle - run: ./gradlew bundleRelease - - name: Build APKS - run: ./gradlew assembleRelease \ No newline at end of file + - name: Build debug APK (community-safe) + shell: powershell + run: .\gradlew.bat createGameZip assembleCommunityDebug --no-daemon \ No newline at end of file diff --git a/.github/workflows/build_and_sign.yml b/.github/workflows/build_and_sign.yml index 222660419..f957927b7 100644 --- a/.github/workflows/build_and_sign.yml +++ b/.github/workflows/build_and_sign.yml @@ -8,19 +8,18 @@ on: jobs: build: - - runs-on: ubuntu-latest + # Runner GAMING labels: self-hosted, Windows, X64 + runs-on: [self-hosted, Windows, X64] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'true' - - name: set up JDK 18 - uses: actions/setup-java@v3 + - name: set up JDK 17 + uses: actions/setup-java@v5 with: - java-version: '18' + java-version: '17' distribution: 'temurin' - cache: gradle - name: Decode Keystore id: decode_keystore @@ -35,48 +34,48 @@ jobs: fileDir: '.' encodedString: ${{ secrets.GOOGLE_SERVICES_JSON }} - name: Set snapshot - run: | - echo "IS_SNAPSHOT=true" >> "$GITHUB_ENV" if: github.ref == 'refs/heads/dev' - - name: Grant execute permission for gradlew - run: chmod +x gradlew + shell: powershell + run: | + "IS_SNAPSHOT=true" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build APK and app bundle - run: ./gradlew bundleRelease assembleRelease + shell: powershell + run: .\gradlew.bat bundlePlaystoreRelease assemblePlaystoreRelease --no-daemon env: RELEASE_STORE_FILE: ${{ steps.decode_keystore.outputs.filePath }} - RELEASE_STORE_PASSWORD : ${{ secrets.RELEASE_STORE_PASSWORD }} - RELEASE_KEY_ALIAS : ${{ secrets.RELEASE_KEY_ALIAS }} - RELEASE_KEY_PASSWORD : ${{ secrets.RELEASE_KEY_PASSWORD }} + RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} + RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} BUILD_NUMBER: ${{ github.run_number }} - name: Upload crashlytics symbols - run: ./gradlew uploadCrashlyticsSymbolFileRelease + shell: powershell + run: .\gradlew.bat uploadCrashlyticsSymbolFilePlaystoreRelease --no-daemon - name: Upload Artifacts - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: name: AppBundle - path: build/outputs/bundle/release/* + path: build/outputs/bundle/playstoreRelease/* - name: Upload Artifacts - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: name: APK - path: build/outputs/apk/release/* + path: build/outputs/apk/playstore/release/* - name: Set version variables + shell: powershell run: | - VER=$(cat version.txt) - echo "VERSION=$VER" >> $GITHUB_ENV + $ver = (Get-Content version.txt -Raw).Trim() + "VERSION=$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Create/Update Github Snapshot Release - uses: mini-bomba/create-github-release@v1.1.3 + uses: softprops/action-gh-release@v2 if: github.ref == 'refs/heads/dev' with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ env.VERSION }} - body: 'Snapshot release' - files: | - build/outputs/apk/release/*.apk - README.md - clear_attachments: true - prerelease: true + tag_name: ${{ env.VERSION }} name: ${{ env.VERSION }} + prerelease: true + body: 'Snapshot release (self-hosted GAMING)' + files: build/outputs/apk/release/*.apk + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish Snapshot to Play Store uses: swootte/upload-google-play@V1.1.5 if: github.ref == 'refs/heads/dev' diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 000000000..c9fc24208 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,147 @@ +# Pre-release APK on the self-hosted GAMING runner (Windows / X64). +name: Pre-release APK + +on: + push: + tags: + - 'v*-pre-release' + - 'v*-pre-release-*' + workflow_dispatch: + inputs: + build_version: + description: 'BUILD_VERSION / release name (e.g. 0.70.1-pre-release-2)' + required: true + default: '0.70.1-pre-release-2' + +permissions: + contents: write + +jobs: + build-and-release: + # Labels on runner GAMING: self-hosted, Windows, X64 + runs-on: [self-hosted, Windows, X64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Resolve Android SDK (self-hosted) + shell: powershell + run: | + $sdk = $env:ANDROID_HOME + if ([string]::IsNullOrWhiteSpace($sdk)) { $sdk = $env:ANDROID_SDK_ROOT } + if ([string]::IsNullOrWhiteSpace($sdk)) { + $candidates = @( + "$env:LOCALAPPDATA\Android\Sdk", + "$env:USERPROFILE\AppData\Local\Android\Sdk", + "E:\000-lastBeacon_ia\android-sdk", + "C:\Android\Sdk", + "E:\Android\Sdk" + ) + foreach ($c in $candidates) { + if ((Test-Path "$c\platform-tools") -or (Test-Path "$c\platforms")) { + $sdk = $c + break + } + } + } + if ([string]::IsNullOrWhiteSpace($sdk) -or -not (Test-Path $sdk)) { + throw "Android SDK not found. Set ANDROID_HOME on the GAMING runner." + } + "ANDROID_HOME=$sdk" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "ANDROID_SDK_ROOT=$sdk" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + # local.properties uses escaped backslashes (Gradle Properties format). + $escaped = ($sdk -replace '\\', '/') + "sdk.dir=$escaped" | Set-Content -Path local.properties -Encoding ascii + Write-Host "ANDROID_HOME=$sdk" + + - name: Resolve version + shell: powershell + run: | + $ver = "${{ github.event.inputs.build_version }}" + if ([string]::IsNullOrWhiteSpace($ver)) { + $tag = "${{ github.ref_name }}" + if ($tag.StartsWith("v")) { + $ver = $tag.Substring(1) + } elseif ("${{ github.event_name }}" -eq "workflow_dispatch") { + $ver = "0.70.1-pre-release-2" + } else { + $ver = $tag + } + } + $runNumber = [int]"${{ github.run_number }}" + $buildNumber = 1000000 + $runNumber + "BUILD_VERSION=$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "RELEASE_TAG=v$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "BUILD_NUMBER=$buildNumber" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "BUILD_VERSION=$ver BUILD_NUMBER=$buildNumber" + + - name: Build release APK (short path) + shell: powershell + run: | + # Windows ndk-build hits CreateProcess error 87 when the link line is too + # long under GithubActions_Runner\_work\... Copy into a short real path. + $short = "C:\cth-pr" + if (Test-Path $short) { Remove-Item -Recurse -Force $short } + New-Item -ItemType Directory -Path $short | Out-Null + robocopy "$env:GITHUB_WORKSPACE" $short /E /NFL /NDL /NJH /NJS /nc /ns /np + # robocopy exit codes 0-7 are success/partial + if ($LASTEXITCODE -ge 8) { throw "robocopy failed with code $LASTEXITCODE" } + "SHORT_WORKSPACE=$short" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Set-Location $short + # Refresh local.properties for the copied tree + $sdk = $env:ANDROID_HOME + "sdk.dir=$($sdk -replace '\\','/')" | Set-Content -Path local.properties -Encoding ascii + .\gradlew.bat createGameZip assembleCommunityRelease --no-daemon + env: + BUILD_VERSION: ${{ env.BUILD_VERSION }} + IS_SNAPSHOT: 'false' + # Set in "Resolve version" (1000000 + run_number) above SNAPSHOT default. + BUILD_NUMBER: ${{ env.BUILD_NUMBER }} + + - name: Stage APK artifact + shell: powershell + run: | + $root = if ($env:SHORT_WORKSPACE) { $env:SHORT_WORKSPACE } else { $env:GITHUB_WORKSPACE } + Set-Location $root + New-Item -ItemType Directory -Force -Path distribution/pre-release | Out-Null + $src = Get-ChildItem -Recurse build/outputs/apk/community/release/*.apk | Select-Object -First 1 + if (-not $src) { + $src = Get-ChildItem -Recurse build/outputs/apk/**/release/*.apk | Select-Object -First 1 + } + if (-not $src) { throw "No release APK found" } + $dest = "distribution/pre-release/CorsixTH-Android-$($env:BUILD_VERSION).apk" + Copy-Item $src.FullName $dest -Force + Write-Host "Staged $dest ($($src.Length) bytes)" + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: CorsixTH-Android-${{ env.BUILD_VERSION }} + path: | + distribution/pre-release/CorsixTH-Android-${{ env.BUILD_VERSION }}.apk + distribution/pre-release/0.70.1-pre-release.md + + - name: Publish / update GitHub pre-release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.RELEASE_TAG }} + name: ${{ env.BUILD_VERSION }} + prerelease: true + generate_release_notes: false + body_path: distribution/pre-release/0.70.1-pre-release.md + files: | + distribution/pre-release/CorsixTH-Android-${{ env.BUILD_VERSION }}.apk + distribution/pre-release/0.70.1-pre-release.md + fail_on_unmatched_files: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitmodules b/.gitmodules index e89f1dfd4..e6cc16f1c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "jni/CorsixTH"] path = jni/CorsixTH - url = https://github.com/alanwoolley/CorsixTH.git - branch = Android + url = https://github.com/thomasbonsirven/CorsixTH-android-0.70.1.git + branch = feature/android-phase4-0.70.1 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cb632e103..4d857ae5c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,15 +9,35 @@ android:smallScreens="false" android:xlargeScreens="true" /> + + + + + + + + + + + + + + + + + + tools:targetApi="36"> @@ -33,6 +53,11 @@ android:name="SDL_ENV.SDL_TOUCH_MOUSE_EVENTS" android:value="false" /> + + + + android:windowSoftInputMode="adjustResize" + tools:targetApi="36"> @@ -70,18 +95,21 @@ + android:exported="false" + android:enableOnBackInvokedCallback="true" /> diff --git a/DEVELOPMENT_POLICY.md b/DEVELOPMENT_POLICY.md new file mode 100644 index 000000000..baf98cabe --- /dev/null +++ b/DEVELOPMENT_POLICY.md @@ -0,0 +1,285 @@ +# CorsixTH Android Modernization — Development Policy + +> Scope: modernization of the Android port toward CorsixTH 0.70.x while preserving a stable, reviewable, reproducible development process. + +## 1. Project objective + +The modernization branch exists to produce a maintainable Android port of CorsixTH based on the current SDL2-era CorsixTH 0.70.x codebase, with modern Android/ARM64 support and a reproducible local build process. + +The first target is **CorsixTH 0.70.1**. SDL3 migration is explicitly out of scope for the first modernization cycle. + +Primary target: + +- Android 14 / 15 / 16 +- `arm64-v8a` first +- local reproducible build on Windows +- Theme Hospital original data imported by the user +- no proprietary Theme Hospital assets committed or redistributed + +## 2. Branch policy + +### Protected baseline + +`dev` is the fork baseline and must remain untouched until the modernization branch reaches an explicit integration gate. + +### Main modernization branch + +Development takes place on: + +`feature/corsixth-0.70-modernization` + +Optional short-lived branches may be created from it for risky or isolated work: + +- `phase/01-local-baseline` +- `phase/02-core-0.70.1` +- `phase/03-native-toolchain` +- `fix/` +- `experiment/` + +### Merge rule + +Nothing is merged into `dev` without explicit owner approval. + +No force-push to `dev`. + +## 3. GitHub Actions policy + +**GitHub Actions MUST NOT be triggered, modified, enabled, rerun, or relied upon without explicit owner approval.** + +Until approval is given: + +- do not push changes directly to `dev` or `master`; +- do not edit `.github/workflows/*`; +- do not manually run any workflow; +- do not rerun failed jobs; +- do not configure a self-hosted runner from GitHub; +- do not publish APK/AAB artifacts through GitHub Actions; +- do not publish to Google Play. + +The current development source of truth is the **local Windows build/test environment**. + +If a future GitHub Actions workflow is prepared, it must remain unexecuted until explicit approval. + +## 4. Change isolation + +Each commit should solve one technical problem or one tightly related group of problems. + +Preferred commit prefixes: + +- `docs:` documentation only +- `build:` Gradle / NDK / toolchain / dependency changes +- `core:` CorsixTH core integration +- `android:` Java/Kotlin/Android platform integration +- `native:` C/C++/JNI/NDK changes +- `input:` touch/mouse/controller changes +- `audio:` SDL_mixer/audio changes +- `storage:` Android file/import/storage changes +- `fix:` bug fix +- `test:` tests or validation tooling +- `refactor:` behavior-preserving restructuring +- `ci:` CI definition only — requires explicit owner approval before use + +Avoid commits mixing unrelated Gradle, C++, UI and gameplay changes. + +## 5. Build-first rule + +Before changing the CorsixTH core, the existing fork must compile locally from a clean checkout. + +A phase is not considered complete because the source "looks correct". It must satisfy its exit criteria in `docs/DEVELOPMENT_PHASES.md`. + +Every build-affecting change must record: + +- command executed; +- Java/JDK version; +- Gradle version; +- Android SDK version; +- Android NDK version; +- ABI; +- build result; +- relevant warnings/errors. + +## 6. Reproducibility + +Do not depend on undocumented machine state. + +Prefer explicit, version-pinned dependencies and toolchains. + +Do not silently depend on: + +- globally installed Gradle when the wrapper exists; +- unknown NDK versions; +- manually copied libraries not documented in the repository; +- untracked patches; +- IDE-only configuration; +- environment variables that are not documented. + +If a manual step is unavoidable, document it before proceeding to the next phase. + +## 7. Native code policy + +CorsixTH 0.70.x requires C++17. The historical Android port currently contains Android-specific native build logic that must be migrated carefully rather than replaced wholesale. + +Rules: + +1. Preserve Android-specific behavior until its purpose is understood. +2. Do not mass-replace the old Android core with upstream files and then fix hundreds of errors blindly. +3. Compare the Android fork and upstream 0.70.1 before porting changes. +4. Keep patches as small and attributable as possible. +5. Prefer upstream-compatible code over Android-only forks when practical. +6. Keep SDL2 for the first modernization milestone. +7. Any SDL3 migration is a later independent project phase. + +## 8. Dependency policy + +For each native dependency, record: + +- current version/source; +- why it is needed; +- ABI support; +- Android compatibility; +- whether it is vendored, built from source or fetched; +- license implications. + +Important dependency families include: + +- SDL2 +- SDL2_mixer +- Lua +- LuaFileSystem +- LPeg +- FreeType +- FFmpeg +- zlib +- libpng +- AndroidX/Kotlin/Room dependencies + +Do not update every dependency at once. + +## 9. Community build policy + +The modernization target is a clean community build. + +Long-term goal: + +- no advertisements; +- no analytics requirement; +- no mandatory Firebase runtime; +- no mandatory Google Play Games dependency; +- no Google Play dependency for core gameplay; +- offline gameplay after game data import; +- local save/load support. + +Removal of Google/Firebase components must be done only after the baseline build is stable, so regressions remain attributable. + +## 10. Proprietary data and legal boundary + +Never commit, upload or redistribute proprietary Theme Hospital game data. + +Do not commit: + +- original game assets; +- GOG installer files; +- proprietary audio/video/data files; +- copyrighted ROM/CD contents; +- private signing keys or keystores. + +The application may provide mechanisms to import user-owned data or supported demo data where legally appropriate. + +## 11. Secrets + +Never commit: + +- keystores; +- passwords; +- tokens; +- service-account JSON; +- API keys; +- private certificates; +- personal environment configuration. + +Local signing configuration must remain outside version control. + +## 12. Testing rule + +Each functional phase must eventually verify, when applicable: + +- APK installs; +- application launches; +- setup wizard works; +- Theme Hospital data imports; +- main menu loads; +- a new game starts; +- sound/music works; +- touch input works; +- save works; +- force-stop/relaunch/load works; +- no obvious crash during a basic gameplay session. + +A compile-only success is not equivalent to a functional success. + +## 13. Android compatibility priority + +Priority order: + +1. `arm64-v8a` +2. Android 16 +3. Android 15 +4. Android 14 +5. modern tablets and phones + +Secondary support can later include: + +- `armeabi-v7a` +- x86_64 emulator/debug + +No legacy ABI should block the primary ARM64 milestone. + +## 14. Regression policy + +If a change breaks a previously validated milestone: + +1. stop forward development; +2. identify the first bad commit; +3. revert or fix the regression; +4. revalidate the previous milestone; +5. only then continue. + +Do not stack new features on top of a broken baseline. + +## 15. Documentation requirement + +Any non-obvious build workaround, Android-specific patch or dependency exception must be documented in the repository before the phase is closed. + +Documentation is part of the implementation, not optional cleanup. + +## 16. Owner approval gates + +Explicit owner approval is required before: + +- using GitHub Actions; +- changing workflow files with intent to run them; +- merging modernization work into `dev`; +- publishing a GitHub Release; +- publishing an APK/AAB publicly; +- publishing to Google Play; +- adding telemetry/analytics; +- changing package identity/signing strategy; +- starting SDL3 migration. + +## 17. Definition of done for the first modernization release + +The initial modernization release is done only when all of the following are true: + +- CorsixTH 0.70.1-based core is integrated; +- project builds reproducibly on the documented Windows environment; +- ARM64 APK installs on modern Android; +- application launches without fatal error; +- original Theme Hospital data can be imported; +- main game loads; +- touch interaction is usable; +- audio is functional; +- save/load is functional; +- Android 14/15/16 behavior is validated or documented; +- no proprietary game data is shipped; +- known blockers are documented; +- owner approves integration/release. diff --git a/README.md b/README.md index f6f2b2fdc..8a26de0c7 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,361 @@ -[![Crowdin](https://d322cqt584bo4o.cloudfront.net/corsixth-for-android/localized.png)](https://crowdin.com/project/corsixth-for-android) [![Snapshot](https://github.com/alanwoolley/CorsixTH-Android/actions/workflows/build_and_sign.yml/badge.svg)](https://github.com/alanwoolley/CorsixTH-Android/actions/workflows/build_and_sign.yml) +# CorsixTH Android — Community Modernization Fork -# CorsixTH for Android +> **Status: active modernization work / not release-ready yet** +> +> This repository is a community-maintained fork of the historical Android port of +> [CorsixTH](https://github.com/CorsixTH/CorsixTH), with the goal of bringing Theme Hospital back to modern Android devices using a maintainable, reproducible and privacy-friendly build. -This is a port of CorsixTH (https://github.com/CorsixTH/CorsixTH) to Android. +## Why this fork exists -You will need either a copy of the original Theme Hospital game or a demo of Theme Hospital in order to use this port. The demo can be automatically downloaded from within the app. +The original Android port of CorsixTH provided an excellent native way to play Theme Hospital on Android, but its Android-specific integration has fallen behind the current CorsixTH engine. -## Credits +The objective of this fork is **not** to rewrite CorsixTH from scratch. The objective is to preserve the existing Android work, modernize it carefully, and reconnect it to a recent upstream CorsixTH core. -A list of all contributors to this project can be found here: [AUTHORS.md](https://github.com/alanwoolley/CorsixTH-Android/blob/dev/Authors.md). +The initial technical target is: -Everyone who's worked on the CorsixTH project deserves a mention here. You can see a complete list of contributors to the CorsixTH project [here](https://github.com/CorsixTH/CorsixTH/blob/master/LICENSE.txt) +- **CorsixTH 0.70.1** as the first modern engine baseline; +- **SDL2** for the first modernization cycle; +- **C++17** for the native engine; +- **ARM64 (`arm64-v8a`)** as the primary architecture; +- modern Android support, with Android **14 / 15 / 16** as priority validation targets; +- reproducible local builds on **Windows 11**; +- a build that can work without mandatory Firebase, Google Analytics or Google Play Services; +- preservation of Theme Hospital import, touch controls, audio, saves and existing Android-specific behavior. -### Translations +CorsixTH 0.70.x is intentionally used as the first target before any SDL3 migration. Moving the engine forward and migrating the multimedia stack at the same time would make regressions much harder to isolate. -All the translations in this project are provided by the community. If you are able to help then please visit the [CorsixTH-Android project on Crowdin](https://crowdin.com/project/corsixth-for-android). +--- -## Download +## Current state -The latest version can be downloaded from [GitHub Releases](https://github.com/alanwoolley/CorsixTH-Android/releases) , or the [Google Play Store](https://play.google.com/store/apps/details?id=uk.co.armedpineapple.cth). +The repository currently still contains the historical Android architecture and has **not yet completed the migration to CorsixTH 0.70.1**. -### Pre-release versions +Current inherited baseline: -Snapshot versions are available from [GitHub Releases](https://github.com/alanwoolley/CorsixTH-Android/releases) and the [Google Play Store](https://play.google.com/store/apps/details?id=uk.co.armedpineapple.cth). These represent active development and may have numerous bugs or imcomplete features. +- Android Gradle Plugin: **8.7.3** +- Gradle wrapper: **8.9** +- Kotlin: **2.0.21** +- `compileSdk`: **35** +- `targetSdk`: **35** +- `minSdk`: **27** / Android 8.1 +- native build: **Android NDK / ndk-build** +- native STL: `c++_static` +- historical CorsixTH Android submodule: `alanwoolley/CorsixTH`, branch `Android` -If you wish to download from the Play Store, you will need to first enrol in the testing channel before download: +The modernization work is being developed on: -- [Become a tester](https://play.google.com/apps/testing/uk.co.armedpineapple.cth) -- [Download from the Play Store](https://play.google.com/store/apps/details?id=uk.co.armedpineapple.cth) +```text +feature/corsixth-0.70-modernization +``` -## Requirements +The default `dev` branch remains the clean fork baseline until the modernization branch reaches defined validation milestones. -Any modern Android phone or tablet should be able to play the game well, however it is best played on a large screen device. +--- -The minimum requirements are: -- At least a 640x480 display -- Android 8.1 (Oreo) or higher +## Project goals +### 1. Obtain a reproducible baseline build -## Installing +Before changing the engine, the historical Android project must build locally from a clean checkout with all required submodules. -CorsixTH requires the original Theme Hospital game files in order to play. If you do not own a version of Theme Hospital, then you can download the demo from within the app, or provide your own copy. +The first success criterion is deliberately simple: -If you have bought the game from [gog.com](https://www.gog.com/en/game/theme_hospital) then you can download the installer on your device, or transfer it to your device, and select the option from the setup wizard that appears on first launch. +```text +Clean checkout + ↓ +Submodules initialized + ↓ +Local Android toolchain + ↓ +assembleDebug + ↓ +Installable APK +``` -If you have a version of Theme Hospital from a different source, you will need to either copy the entire directory to your device, or make it available on a cloud storage provider. You can then import the files from the setup wizard. +No engine upgrade should be considered validated until this baseline is reproducible. +### 2. Decouple the community build from Google services +The historical project currently integrates Firebase, Crashlytics, Firebase Performance and Google Play Games. + +The long-term goal is to provide a community build that can be compiled and used locally without requiring: + +- `google-services.json`; +- Firebase Analytics; +- Firebase Crashlytics; +- Firebase Performance; +- Google Play Games; +- Google Play publishing credentials. + +A future split may use dedicated build variants such as: + +```text +communityDebug +communityRelease +googlePlayRelease +``` + +The community flavor must remain fully functional without proprietary service credentials. + +### 3. Upgrade the CorsixTH core to 0.70.1 + +The Android wrapper currently relies on a dedicated historical CorsixTH Android branch. + +The modernization phase will compare the Android-specific patches against upstream CorsixTH 0.70.1 and progressively port only the required Android adaptations. + +Key migration areas include: + +- C++11 → **C++17**; +- native source layout changes; +- Lua integration; +- SDL2 integration; +- SDL2_mixer; +- FreeType; +- FFmpeg / movie playback; +- LuaFileSystem / LPeg; +- Android storage integration; +- Android lifecycle integration; +- touch and pointer handling. + +### 4. Preserve Android-specific functionality + +A successful compile alone is not sufficient. + +The following must continue to work: + +- first-launch setup; +- Theme Hospital data import; +- GOG installer import where supported; +- demo data workflow where legally available; +- touch controls; +- mouse input; +- audio and music; +- video playback where supported; +- UI scaling; +- save / load; +- application suspend / resume; +- Android back navigation; +- file access on recent Android versions. + +### 5. Make Android 16 and ARM64 first-class targets + +The first supported modernization target is **ARM64**. + +Priority ABI: + +```text +arm64-v8a +``` + +Other ABIs may be retained or restored later if they do not significantly complicate maintenance. + +The project will be validated primarily against modern Android storage, lifecycle and permission behavior rather than preserving obsolete Android compatibility at all costs. + +--- + +## Development roadmap + +The work is split into controlled phases so that regressions can be attributed to a specific change. + +| Phase | Objective | Exit criterion | +|---|---|---| +| **0** | Repository preparation | Branching, documentation and development rules established | +| **1** | Historical local build | Clean checkout produces a debug APK locally | +| **2** | Reproducible toolchain | JDK, Gradle, SDK and NDK versions documented and repeatable | +| **3** | Community build cleanup | Local build no longer requires Google/Firebase credentials | +| **4** | CorsixTH 0.70.1 bootstrap | New core is integrated far enough to compile native code | +| **5** | Native migration | C++17 and native dependencies compile successfully | +| **6** | First launch | Application starts with the 0.70.1-based core | +| **7** | Gameplay validation | Theme Hospital data loads and a playable game can start | +| **8** | Android integration | Touch, storage, lifecycle, audio and save/load are validated | +| **9** | Android 14/15/16 hardening | Modern Android regressions are resolved | +| **10** | Community release | Signed, documented ARM64 release candidate is produced | +| **11+** | Future work | SDL3 and larger architectural modernization, only after stability | + +Detailed phase definitions are maintained in: + +- [`docs/DEVELOPMENT_PHASES.md`](docs/DEVELOPMENT_PHASES.md) +- [`docs/PROJECT_STATUS.md`](docs/PROJECT_STATUS.md) + +--- + +## Development policy + +This repository follows a conservative modernization strategy. + +Important rules include: + +- do not develop directly on `dev`; +- use focused feature branches; +- prefer small, reviewable and reversible commits; +- do not mix engine migration, SDL migration and UI redesign in the same phase; +- do not remove historical Android-specific code until its purpose is understood; +- do not silently change save compatibility; +- do not commit proprietary Theme Hospital assets; +- do not commit signing keys, service credentials or secrets; +- every phase must have an explicit validation criterion before moving forward. + +The complete policy is documented in: + +- [`DEVELOPMENT_POLICY.md`](DEVELOPMENT_POLICY.md) + +Rules intended for AI-assisted development are documented in: + +- [`docs/AI_AGENT_RULES.md`](docs/AI_AGENT_RULES.md) + +AI agents working on this repository are expected to diagnose one root problem at a time, preserve working behavior, document uncertainty, and avoid broad speculative rewrites. + +--- + +## Build and CI strategy + +### Local-first development + +This fork is intentionally using a **local-first build strategy** during the modernization phase. + +GitHub Actions must **not** be treated as the primary development loop. Frequent compilation and validation are expected to run locally on Windows instead. + +The intended development environment is: + +```text +Windows 11 + │ + ├─ Git + ├─ JDK + ├─ Android SDK + ├─ Android NDK + ├─ Gradle wrapper + ├─ ADB + └─ optional WSL2 / Docker / act +``` + +A local GitHub Actions-compatible workflow may later be reproduced with [`act`](https://github.com/nektos/act), but the authoritative requirement is that the project can be built directly with the Gradle wrapper. + +Typical target command: + +```powershell +.\gradlew.bat assembleDebug +``` + +### GitHub Actions policy + +The upstream repository contains historical GitHub Actions workflows. During modernization, **remote GitHub Actions are not to be enabled, modified or relied upon without explicit project-owner approval**. + +This prevents unnecessary CI usage while the project is expected to compile very frequently during migration work. + +Once the local build is stable, CI can be introduced as a validation layer rather than as the development environment itself. + +--- + +## What this project is not + +This project is **not**: + +- a distribution of Theme Hospital; +- a source for proprietary Bullfrog / Electronic Arts assets; +- an APK archive of abandoned commercial software; +- a rewrite of Theme Hospital; +- a replacement for the upstream CorsixTH project; +- currently a stable production release. + +The purpose of this repository is to maintain the **Android integration layer** required to run CorsixTH with legally obtained Theme Hospital data. + +--- + +## Theme Hospital data requirement + +CorsixTH requires Theme Hospital game data in order to play the full game. + +This repository does **not** contain Theme Hospital assets. + +Users must provide their own legally obtained game files. Historically the Android port also supported importing supported GOG installers and, where available, using the Theme Hospital demo data. + +Modernization work must preserve a clear separation between: + +```text +Open-source CorsixTH / Android code + + +User-provided Theme Hospital data + = +Playable game +``` + +--- + +## Future direction + +Once the 0.70.1 / SDL2 Android baseline is stable, possible future work includes: + +- migration to newer CorsixTH releases; +- SDL3 evaluation; +- further Android 16+ compatibility work; +- improved touch gestures and phone ergonomics; +- tablet-specific UI improvements; +- optional gamepad improvements; +- simplified Theme Hospital data import; +- cleaner dependency management; +- migration away from legacy `ndk-build` if there is a clear maintenance benefit; +- reproducible release signing and checksums; +- automated local regression tests; +- optional upstreaming of generic Android improvements where appropriate. + +These are intentionally secondary goals. **The first priority is a stable, maintainable CorsixTH 0.70.1 Android port.** + +--- + +## Upstream projects and credits + +This fork exists because of the work done by the original projects and their contributors. + +### CorsixTH + +Main engine: + +- https://github.com/CorsixTH/CorsixTH + +CorsixTH is an open-source reimplementation of the Theme Hospital engine. All CorsixTH contributors deserve credit for the engine, gameplay compatibility and continued development of the project. + +### Original Android port + +This repository was forked from: + +- https://github.com/alanwoolley/CorsixTH-Android + +The original Android port contains the Android application layer, setup workflow, native Android integration and mobile-specific adaptations that form the technical foundation of this modernization effort. + +Existing contributor information is retained in [`AUTHORS.md`](AUTHORS.md). + +### Theme Hospital + +Theme Hospital was originally developed by Bullfrog Productions and published by Electronic Arts. + +This project is an independent community project and does not distribute the original commercial game data. + +--- + +## Project documentation + +Start here if you want to understand or work on the modernization: + +- [`DEVELOPMENT_POLICY.md`](DEVELOPMENT_POLICY.md) — development rules and constraints +- [`docs/DEVELOPMENT_PHASES.md`](docs/DEVELOPMENT_PHASES.md) — detailed migration phases +- [`docs/PROJECT_STATUS.md`](docs/PROJECT_STATUS.md) — current technical status +- [`docs/MIGRATION_MATRIX_0.70.1.md`](docs/MIGRATION_MATRIX_0.70.1.md) — Android fork vs CorsixTH 0.70.1 delta +- [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](docs/PHASE3_NATIVE_TOOLCHAIN.md) — ndk-build / C++17 / ARM64 readiness +- [`docs/AI_AGENT_RULES.md`](docs/AI_AGENT_RULES.md) — rules for AI-assisted development + +--- + +## Contributions + +The project is currently in an early modernization phase. Contributions are useful, but changes should follow the documented phase currently in progress rather than introducing unrelated refactors. + +Before proposing a large change: + +1. read the development policy; +2. check the current project phase; +3. keep the change narrowly scoped; +4. preserve Android-specific behavior unless intentionally replacing it; +5. provide build or runtime evidence whenever possible. + +The project favors **measurable progress and reproducibility over large rewrites**. diff --git a/build.gradle b/build.gradle index ab6124d9d..6bcfaa792 100644 --- a/build.gradle +++ b/build.gradle @@ -22,9 +22,29 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: 'versioning.gradle' apply plugin: 'kotlin-kapt' -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' -apply plugin: 'com.google.firebase.firebase-perf' + +// Phase 9: Google/Firebase plugins only when building the playstore flavor +// (or when a real non-stub google-services.json is present for IDE sync). +def communityGoogleServicesMarker = 'corsixth-android-community-local' +def googleServicesJson = file('google-services.json') +def taskNamesLower = gradle.startParameter.taskNames.collect { it.toLowerCase() } +def buildingPlaystore = taskNamesLower.any { it.contains('playstore') } +def hasRealGoogleServices = googleServicesJson.exists() && + !googleServicesJson.text.contains(communityGoogleServicesMarker) +def enablePlaystoreGooglePlugins = buildingPlaystore || hasRealGoogleServices + +if (enablePlaystoreGooglePlugins) { + if (!googleServicesJson.exists()) { + throw new GradleException( + "Play Store build requires google-services.json (gitignored private file).") + } + apply plugin: 'com.google.gms.google-services' + apply plugin: 'com.google.firebase.crashlytics' + apply plugin: 'com.google.firebase.firebase-perf' + logger.lifecycle("Play Store Google/Firebase plugins enabled.") +} else { + logger.lifecycle("Community configuration: Google Services / Crashlytics / Perf plugins skipped.") +} repositories { google() @@ -56,13 +76,6 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.16.0' - implementation 'com.google.firebase:firebase-crashlytics:19.3.0' - implementation 'com.google.firebase:firebase-crashlytics-ndk:19.3.0' - implementation 'com.google.firebase:firebase-analytics:22.1.2' - implementation 'com.google.firebase:firebase-perf:21.0.3' - - implementation 'com.google.android.gms:play-services-games-v2:20.1.2' - def room_version = "2.6.1" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" @@ -77,12 +90,35 @@ configurations.configureEach { android { namespace 'uk.co.armedpineapple.cth' + // Pinned to the NDK version verified by the Windows Phase-1 baseline build. + // Prefer this over deprecated local.properties ndk.dir. + ndkVersion "27.0.12077973" defaultConfig { minSdkVersion 27 - targetSdkVersion 35 + // Phase 8: target Android 16 behavior (edge-to-edge, 16 KB pages, etc.). + targetSdkVersion 36 versionName = project.ext.buildVersion versionCode = project.ext.offsetBuildVersionCode + + // Phase 3: enforce the ARM64 modernization ABI at the AGP layer as well + // as in jni/Application.mk (prevents packaging stale multi-ABI .so files). + ndk { + abiFilters "arm64-v8a" + } + } + + flavorDimensions = ["distribution"] + productFlavors { + community { + dimension "distribution" + isDefault true + buildConfigField "boolean", "COMMUNITY_BUILD", "true" + } + playstore { + dimension "distribution" + buildConfigField "boolean", "COMMUNITY_BUILD", "false" + } } compileOptions { @@ -94,9 +130,11 @@ android { } signingConfigs { - if (System.getenv('RELEASE_STORE_FILE') != null) { + // Treat blank env as unset (GHA may inject empty secret strings). + def releaseStoreFile = System.getenv('RELEASE_STORE_FILE') + if (releaseStoreFile != null && !releaseStoreFile.trim().isEmpty()) { release { - storeFile file(System.getenv("RELEASE_STORE_FILE")) + storeFile file(releaseStoreFile) storePassword System.getenv("RELEASE_STORE_PASSWORD") keyAlias System.getenv("RELEASE_KEY_ALIAS") keyPassword System.getenv("RELEASE_KEY_PASSWORD") @@ -107,13 +145,16 @@ android { buildTypes { release { buildConfigField "boolean", "ALWAYS_UPGRADE", "false" - if (System.getenv('RELEASE_STORE_FILE') != null) { + def releaseStoreFile = System.getenv('RELEASE_STORE_FILE') + if (releaseStoreFile != null && !releaseStoreFile.trim().isEmpty()) { signingConfig signingConfigs.release } else { signingConfig signingConfigs.debug } - firebaseCrashlytics { - nativeSymbolUploadEnabled true + if (enablePlaystoreGooglePlugins) { + firebaseCrashlytics { + nativeSymbolUploadEnabled true + } } } @@ -131,6 +172,13 @@ android { } } + // SDL dlopen(nativeLibraryDir/libappmain.so) needs extracted .so files. + packaging { + jniLibs { + useLegacyPackaging = true + } + } + applicationVariants.configureEach { variant -> tasks["merge${variant.name.capitalize()}Assets"] .dependsOn(createGameZip) @@ -154,9 +202,28 @@ android { abortOnError false checkReleaseBuilds false } - compileSdk 35 + compileSdk 36 } +// Phase 9: cloud / Play Store-only dependencies (after productFlavors exist). +dependencies { + playstoreImplementation 'com.google.firebase:firebase-crashlytics:19.3.0' + playstoreImplementation 'com.google.firebase:firebase-crashlytics-ndk:19.3.0' + playstoreImplementation 'com.google.firebase:firebase-analytics:22.1.2' + playstoreImplementation 'com.google.firebase:firebase-perf:21.0.3' + playstoreImplementation 'com.google.android.gms:play-services-games-v2:20.1.2' +} + +// Disable playstore variants unless Google plugins/credentials are available so +// `assembleDebug` does not accidentally require a private google-services.json. +androidComponents { + beforeVariants(selector().all()) { variantBuilder -> + if (variantBuilder.name.toLowerCase().startsWith("playstore") && + !enablePlaystoreGooglePlugins) { + variantBuilder.enable = false + } + } +} tasks.register('saveVersion') { new File(projectDir, "version.txt").text = project.ext.buildVersion @@ -164,7 +231,7 @@ tasks.register('saveVersion') { preBuild.dependsOn(saveVersion) -// Create ZIP of CorsixTH game assets +// Create ZIP of CorsixTH game assets (engine/Lua only — never Theme Hospital proprietary data) tasks.register('createGameZip', Zip) { destinationDirectory = file('assets') @@ -176,8 +243,13 @@ tasks.register('createGameZip', Zip) { include 'Lua/**' include 'CorsixTH.lua' include 'Bitmap/**' - include 'Campaigns/**' include 'Graphics/**' + + // Bitmap build helpers are not needed at runtime on device. + exclude 'Bitmap/mk*.lua' + exclude 'Bitmap/lib_*.lua' + exclude 'Bitmap/readme.txt' + exclude 'Bitmap/example.spec' } } @@ -199,4 +271,4 @@ class RoomSchemaArgProvider implements CommandLineArgumentProvider { Iterable asArguments() { return ["room.schemaLocation=${schemaDir.path}".toString()] } -} \ No newline at end of file +} diff --git a/distribution/community/google-services.community.json b/distribution/community/google-services.community.json new file mode 100644 index 000000000..b04176bfc --- /dev/null +++ b/distribution/community/google-services.community.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "000000000000", + "project_id": "corsixth-android-community-local", + "storage_bucket": "corsixth-android-community-local.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000", + "android_client_info": { + "package_name": "uk.co.armedpineapple.cth" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCommunityLocalBuildOnlyPlaceholder000" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/distribution/pre-release/0.70.1-pre-release.md b/distribution/pre-release/0.70.1-pre-release.md new file mode 100644 index 000000000..4dfb285c7 --- /dev/null +++ b/distribution/pre-release/0.70.1-pre-release.md @@ -0,0 +1,87 @@ +# CorsixTH-Android 0.70.1-pre-release-2 + +**Branch:** `feature/corsixth-0.70-modernization` +**Tag:** `v0.70.1-pre-release-2` +**Flavor:** `community` (no Firebase / Play Games) + +> ## ⚠ NOT FULLY TESTED / PAS ENCORE ENTIÈREMENT TESTÉ +> +> **English:** This build includes Phases 0–9. It is a **pre-release for early testers**. +> Functional smoke has been done on a limited set of devices; **Android 14 / 15 / 16 matrix, long play sessions, and edge cases are not fully signed off.** Expect bugs. Please report crashes and regressions. +> +> **Français :** Cette build inclut les phases 0–9. C’est une **pre-release pour testeurs précoces**. +> Des smokes limités ont été faits ; **la matrice Android 14 / 15 / 16, les longues sessions et les cas limites ne sont pas entièrement validés.** Des bugs sont possibles. Merci de signaler crashes et régressions. + +--- + +## English + +### Highlights +- CorsixTH **0.70.1** on Android (**ARM64** / `arm64-v8a` only). +- Modern toolchain: AGP 8.x, Gradle 8.9, Kotlin 2.0, NDK 27, C++17, **target/compile SDK 36**. +- Theme Hospital import via SAF (GOG / retail); engine via `game.zip`. +- Soft keyboard for player name and textboxes (IME bridge). +- **Community flavor**: no Firebase, Crashlytics, Analytics, or Play Games in the APK. + +### What was done + +| Area | Details | +|---|---| +| Phases 0–7 | Baseline, 0.70.1 bootstrap, data packaging, gameplay UI, save/load lifecycle, soft keyboard | +| Phase 8 | Android 14/15/16 hardening: SDK 36, SAF-only storage, exported components, HID receivers, edge-to-edge/cutout, 16 KB page-size support, backup rules | +| Phase 9 | `community` / `playstore` flavors; community APK builds without Google cloud SDKs or telemetry | +| Keyboard | IME via `GameActivity` EditText + `CthImeBridge`; empty-buffer backspace deletes pre-filled `PLAYER` | + +### Keyboard fix (recap) +- **Problem:** Soft keyboard missing on “Player name”; `PLAYER` could not be deleted. +- **Fix:** `require("TH")` / `require("sdl")` in `window.lua`; EditText IME bridge; backspace forwarding; SDL `showTextInput` → GameActivity path. + +### Not in this pre-release +- Phase 10 release engineering / Play Store publishing polish +- SDL3 / MIDI (deferred) +- Full automated device test matrix + +### Install notes +- **Pre-release**, typically debug-signed unless CI release secrets are configured. +- Theme Hospital original data still required (setup wizard / SAF). +- Prefer a clean install if upgrading from older debug builds. + +--- + +## Français + +### Points clés +- Moteur CorsixTH **0.70.1** sur Android (**ARM64** / `arm64-v8a` uniquement). +- Chaîne moderne : AGP 8.x, Gradle 8.9, Kotlin 2.0, NDK 27, C++17, **SDK 36**. +- Import Theme Hospital via SAF ; moteur via `game.zip`. +- Clavier logiciel (pont IME). +- Flavor **community** : pas de Firebase / Crashlytics / Analytics / Play Games dans l’APK. + +### Réalisé + +| Domaine | Détails | +|---|---| +| Phases 0–7 | Baseline, bootstrap 0.70.1, données, UI, save/load, clavier | +| Phase 8 | Durcissement Android 14/15/16 : SDK 36, SAF, composants exportés, receivers HID, edge-to-edge, pages 16 Ko, backup | +| Phase 9 | Flavors `community` / `playstore` ; APK community sans SDK cloud Google ni télémétrie | +| Clavier | Pont `EditText` + `CthImeBridge` ; effacement de `PLAYER` prérempli | + +### Correctif clavier (rappel) +- **Problème :** clavier absent / impossible d’effacer `PLAYER`. +- **Correction :** `require` Lua, pont IME, retour arrière, chemin `GameActivity`. + +### Hors périmètre +- Phase 10 engineering release / publication Play Store +- SDL3 / MIDI (reportés) +- Matrice de tests automatisée complète + +### Installation +- **Pre-release**, signature debug sauf secrets CI de release. +- Données Theme Hospital toujours requises. +- Installation propre recommandée après d’anciennes builds debug. + +--- + +## Artifact + +- `CorsixTH-Android-0.70.1-pre-release-2.apk` (arm64-v8a, **community**) diff --git a/docs/AI_AGENT_RULES.md b/docs/AI_AGENT_RULES.md new file mode 100644 index 000000000..7c199ace0 --- /dev/null +++ b/docs/AI_AGENT_RULES.md @@ -0,0 +1,270 @@ +# AI / Automated Development Agent Rules + +This file defines mandatory constraints for any AI agent, coding assistant or automated tool working on the CorsixTH Android modernization branch. + +## 1. Mandatory branch + +Work only on: + +`feature/corsixth-0.70-modernization` + +or on a short-lived child branch explicitly created from it. + +Do not modify `dev` or `master` unless the repository owner explicitly requests it. + +## 2. GitHub Actions prohibition + +Until explicit owner approval: + +- do not trigger GitHub Actions; +- do not manually run workflows; +- do not rerun workflow jobs; +- do not modify workflow files for execution; +- do not merge into a branch whose push will trigger the inherited workflows; +- do not configure a GitHub-hosted or self-hosted runner; +- do not publish release artifacts automatically. + +Local Windows build/test is the authoritative validation path. + +## 3. Read before editing + +Before changing code, inspect at minimum: + +- `DEVELOPMENT_POLICY.md` +- `docs/DEVELOPMENT_PHASES.md` +- `docs/PROJECT_STATUS.md` +- `.gitmodules` +- `build.gradle` +- `gradle/wrapper/gradle-wrapper.properties` +- `jni/Application.mk` +- `jni/Android.mk` +- relevant native dependency `Android.mk` files + +When changing the CorsixTH core, inspect the Android fork delta before replacing anything. + +## 4. One phase at a time + +Do not attempt multiple migration phases at once. + +The active phase is recorded in `docs/PROJECT_STATUS.md`. + +Do not start the next phase until the current phase exit criteria are satisfied or the owner explicitly overrides the gate. + +## 5. No speculative mass rewrite + +Forbidden without explicit owner approval: + +- replacing the whole Android application architecture; +- migrating to Jetpack Compose; +- migrating SDL2 to SDL3 during the 0.70.1 modernization; +- replacing `ndk-build` with CMake only because it appears cleaner; +- deleting historical Android-specific code before understanding it; +- updating every dependency simultaneously; +- rewriting Java/Kotlin components unrelated to the active blocker; +- changing package/application identity; +- changing signing strategy; +- adding telemetry. + +Prefer minimal, reversible changes. + +## 6. CorsixTH target + +First modernization target: + +`CorsixTH 0.70.1` + +Do not track upstream `master` for the first stable Android migration. + +Reason: the project must stabilize on the final SDL2-era 0.70.x architecture before any later SDL3 migration. + +## 7. C++ standard + +The modernization target requires C++17. + +When modifying native build settings: + +- identify every place where C++11 is forced; +- change only the settings needed for C++17 compatibility; +- preserve exception/runtime requirements unless evidence shows otherwise; +- record compiler errors before patching source code around them. + +Do not silence compiler errors with broad flags unless the reason is documented. + +## 8. Android-first native patching + +For each native compile error: + +1. capture the exact error; +2. identify whether it comes from upstream 0.70.1, Android glue, dependency version or toolchain; +3. find the smallest valid fix; +4. prefer upstream-compatible logic; +5. build again; +6. document non-obvious Android-specific differences. + +Do not apply large mechanical patches without intermediate builds. + +## 9. Build evidence + +After any build-affecting commit, record or report: + +```text +Commit: +Phase: +Command: +JDK: +Gradle: +Android SDK: +NDK: +ABI: +Result: +First error if failed: +APK produced if successful: +Device test result if performed: +``` + +Never report a build as successful unless the build command actually completed successfully. + +Never report the application as working unless it was installed/launched or the owner explicitly states the runtime test result. + +## 10. Failure handling + +When a build fails: + +- stop at the first meaningful root-cause error; +- do not patch dozens of downstream errors independently; +- determine whether the later errors are cascading failures; +- preserve logs; +- make one focused fix; +- rebuild. + +## 11. Dependency changes + +Any dependency update must answer: + +- Why is this update necessary now? +- What previous version/source was used? +- Does it support `arm64-v8a`? +- Does it alter API/ABI behavior? +- Does it alter licensing? +- Does it require an Android build-system change? + +If not required by the active phase, defer it. + +## 12. Proprietary assets + +Never add Theme Hospital proprietary data to Git. + +Never add: + +- original game data; +- GOG installer; +- copied CD files; +- copyrighted game audio/video/assets. + +Tests should use local user-owned data outside the repository. + +## 13. Secrets and signing + +Never commit or print: + +- GitHub tokens; +- passwords; +- Android keystores; +- keystore passwords; +- Firebase service credentials; +- Google service-account credentials; +- private API keys. + +Use local environment variables/files excluded from Git when needed. + +## 14. Firebase/Google cleanup timing + +Do not remove Firebase/Google dependencies during baseline reproduction or initial core migration unless they directly prevent the build. + +First establish a working baseline and 0.70.1 bootstrap. + +Then isolate/remove unnecessary cloud dependencies in the dedicated community-build phase. + +This preserves regression traceability. + +## 15. Generated files + +Do not commit generated binaries or local build outputs unless explicitly requested. + +Typical files that should remain untracked include: + +- APK/AAB outputs; +- Gradle caches; +- `.gradle/`; +- local SDK paths; +- native object files; +- local signing material; +- imported Theme Hospital game data. + +## 16. Commit quality + +Each commit must be: + +- build-oriented or logically atomic; +- understandable from its message; +- reversible; +- free of unrelated formatting churn; +- free of generated/proprietary files. + +Preferred examples: + +```text +build: pin local NDK version +native: enable C++17 for Android core +core: forward-port Android bootstrap to CorsixTH 0.70.1 +storage: adapt game data import to scoped storage +fix: restore SDL audio resume after activity pause +``` + +Avoid messages such as: + +```text +fix stuff +update +working changes +AI changes +misc +``` + +## 17. Documentation updates + +When a phase changes state, update `docs/PROJECT_STATUS.md`. + +When a new non-obvious workaround is introduced, document it in the repository in the same change set or immediately after it. + +## 18. Definition of trustworthy output + +An AI agent must clearly distinguish: + +- **verified**: observed in repository/build/device output; +- **inferred**: strongly suggested by code or documentation; +- **proposed**: a recommended future change; +- **untested**: implemented but not validated. + +Never present inferred or untested behavior as verified. + +## 19. Stop conditions requiring owner decision + +Stop and request owner direction before: + +- triggering GitHub Actions; +- merging into `dev`; +- publishing anything; +- changing application/package identity; +- deleting compatibility code whose purpose is unclear; +- replacing the native build system; +- starting SDL3 migration; +- introducing telemetry; +- introducing a major dependency with licensing implications; +- committing a workaround that materially diverges from upstream CorsixTH behavior. + +## 20. Primary engineering principle + +The goal is not to modernize everything at once. + +The goal is to move from a known Android port to a **working, reproducible, maintainable CorsixTH 0.70.1 Android build** through small, testable steps. diff --git a/docs/DEVELOPMENT_PHASES.md b/docs/DEVELOPMENT_PHASES.md new file mode 100644 index 000000000..cb4d17b8b --- /dev/null +++ b/docs/DEVELOPMENT_PHASES.md @@ -0,0 +1,422 @@ +# CorsixTH Android Modernization — Development Phases + +This document defines the ordered implementation plan and the exit criteria for each phase. + +**Rule:** do not skip a failed phase by compensating with later changes. + +## Phase 0 — Baseline and repository control + +### Goal + +Establish a safe modernization branch and document the inherited state before changing behavior. + +### Tasks + +- Keep `dev` untouched as the fork baseline. +- Work from `feature/corsixth-0.70-modernization`. +- Inventory current Gradle, SDK, NDK, Kotlin, Java and native build configuration. +- Record current submodules and their exact revisions. +- Record current Android-specific patches in the CorsixTH submodule/fork. +- Inventory existing GitHub workflows without executing them. +- Confirm no proprietary Theme Hospital data exists in the repository. + +### Exit criteria + +- Development policy exists. +- Phase plan exists. +- AI/agent rules exist. +- Current status document exists. +- Baseline revisions are recorded. +- No GitHub Actions have been triggered intentionally. + +--- + +## Phase 1 — Reproducible local build of the inherited port + +### Goal + +Build the current fork locally on Windows before changing the CorsixTH core. + +### Tasks + +- Define the supported local Windows build path. +- Install/pin required JDK. +- Use repository Gradle wrapper. +- Install/pin Android SDK components. +- Install/pin Android NDK. +- Initialize/update submodules. +- Resolve local-only Google/Firebase configuration requirements without committing secrets. +- Build debug APK first. +- Build release-like unsigned/debug-signed APK if practical. +- Install APK using ADB. + +### Required evidence + +Record: + +```text +Windows version: +JDK: +Gradle wrapper: +Android SDK: +Build Tools: +NDK: +ABI: +Command: +Result: +APK path: +Device Android version: +``` + +### Exit criteria + +- Clean clone can produce an APK locally. +- APK installs on at least one ARM64 Android device. +- Application reaches its setup flow or expected first screen. +- Procedure is documented and repeatable. + +--- + +## Phase 2 — Android fork vs CorsixTH 0.70.1 delta analysis + +### Goal + +Understand exactly what must be ported before replacing the historical Android core. + +### Tasks + +- Compare current Android CorsixTH fork with upstream CorsixTH 0.70.1. +- Classify Android-specific changes into: + - bootstrap/startup; + - file paths/storage; + - input/touch; + - logging; + - SDL lifecycle; + - audio; + - graphics; + - JNI glue; + - platform-specific macros; + - Lua hooks; + - build-only patches. +- Identify upstream changes that make old Android patches obsolete. +- Identify patches that must be forward-ported. + +### Deliverable + +Create a migration matrix containing: + +| Android patch/behavior | Still required? | 0.70.1 equivalent | Migration strategy | Status | +|---|---|---|---|---| + +### Exit criteria + +- No blind submodule replacement is planned. +- All critical Android-specific behavior has an identified migration path. + +--- + +## Phase 3 — Toolchain and native build modernization + +### Goal + +Make the Android native toolchain compatible with the 0.70.x requirements before full core migration. + +### Tasks + +- Move native C++ standard from C++11 to C++17. +- Verify current NDK compatibility. +- Audit `Application.mk` and all `Android.mk` files. +- Confirm `c++_static`/STL strategy. +- Validate ARM64 compilation path. +- Audit include paths and source lists. +- Audit native dependencies individually. +- Keep SDL2. +- Avoid SDL3 migration. + +### Exit criteria + +- Minimal native build environment supports C++17. +- ARM64 target is explicitly supported. +- Native dependency blockers are known and documented. + +--- + +## Phase 4 — CorsixTH 0.70.1 core bootstrap + +### Goal + +Compile the Android application with a 0.70.1-based CorsixTH core. + +### Strategy + +Do not attempt to solve gameplay, input, audio and storage simultaneously. + +Priority: + +1. compile; +2. link; +3. load native library; +4. initialize SDL; +5. initialize Lua; +6. reach CorsixTH bootstrap; +7. reach visible UI. + +### Expected problem categories + +- renamed/removed source files; +- changed C++ interfaces; +- generated CMake/config headers; +- Lua API changes; +- dependency discovery differences; +- FFmpeg API/version differences; +- SDL entry-point differences; +- Android file path assumptions. + +### Exit criteria + +- Android native library builds with the 0.70.1-based core. +- APK installs. +- Application launches without native loader failure. +- CorsixTH initialization reaches a known visible state or a reproducible next blocker. + +--- + +## Phase 5 — Data packaging and Theme Hospital import + +### Goal + +Restore reliable game-data setup on modern Android. + +### Tasks + +- Validate `game.zip` asset generation against 0.70.1 layout. +- Validate bundled CorsixTH Lua/resources. +- Preserve separation between open-source engine data and proprietary Theme Hospital data. +- Validate setup wizard. +- Validate folder/file selection using modern Android storage APIs. +- Validate GOG import path if retained. +- Validate demo import/download path if retained. +- Validate application-private storage destination. + +### Exit criteria + +- Fresh install reaches setup wizard. +- User-owned Theme Hospital data can be imported. +- CorsixTH finds imported data after restart. +- No broad/unnecessary storage permission is required where avoidable. + +--- + +## Phase 6 — Input, UI, audio and gameplay essentials + +### Goal + +Reach a genuinely playable state. + +### Input tests + +- tap = left click; +- drag; +- scrolling/map movement; +- long press/right-click equivalent if applicable; +- pinch/zoom where supported; +- Android back behavior; +- hardware mouse; +- keyboard where relevant; +- controller later if practical. + +### UI tests + +- phone aspect ratios; +- tablet aspect ratios; +- display scaling; +- orientation behavior; +- cutouts/insets if relevant; +- readable dialogs and setup screens. + +### Audio tests + +- music; +- sound effects; +- pause/resume; +- interruption/resume after Android lifecycle events. + +### Gameplay tests + +- start new game; +- place/build room; +- hire staff; +- interact with UI; +- run simulation for a meaningful period. + +### Exit criteria + +- Game is usable without external mouse/keyboard. +- Main audio path works. +- Basic gameplay loop works. + +--- + +## Phase 7 — Save/load and lifecycle reliability + +### Goal + +Ensure the game survives normal Android usage. + +### Tests + +1. Start game. +2. Save. +3. Return to launcher/home. +4. Resume. +5. Force-stop application. +6. Relaunch. +7. Load save. +8. Lock/unlock device. +9. Background/foreground repeatedly. + +### Additional checks + +- save directory persistence; +- upgrade behavior; +- migration from older CorsixTH Android saves if feasible; +- no data loss on application update. + +### Exit criteria + +- Save/load works after process restart. +- Normal Android lifecycle does not cause routine crashes or silent data loss. + +--- + +## Phase 8 — Android 14/15/16 hardening + +### Goal + +Remove compatibility debt specific to modern Android. + +### Audit areas + +- target/compile SDK behavior changes; +- scoped storage; +- foreground/background restrictions; +- package visibility if relevant; +- notification permissions if any remain; +- exported components; +- pending intents; +- file URI/content URI handling; +- native page-size compatibility where applicable; +- edge-to-edge/insets; +- deprecated APIs; +- ARM64-only device behavior. + +### Exit criteria + +- Functional test on Android 14. +- Functional test on Android 15. +- Functional test on Android 16. +- Known device-specific limitations documented. + +--- + +## Phase 9 — Community build cleanup + +### Goal + +Decouple core gameplay from unnecessary Google/Firebase services. + +### Tasks + +Evaluate and progressively isolate/remove: + +- Firebase Analytics; +- Firebase Performance; +- Crashlytics; +- Google Services plugin; +- Google Play Games dependency; +- Play Store publication-only code. + +Possible future product flavors: + +```text +community +playstore +``` + +The community flavor should be capable of functioning without Google services. + +### Exit criteria + +- Community APK builds without unnecessary cloud dependencies. +- Core gameplay works offline. +- No telemetry is introduced without explicit owner approval. + +--- + +## Phase 10 — Release engineering + +### Goal + +Prepare a clean, reproducible, owner-approved package. + +### Tasks + +- deterministic/documented local build process; +- release signing process documented but secrets external; +- semantic versioning strategy; +- changelog; +- SHA-256 checksums; +- APK naming convention; +- license/attribution audit; +- release notes; +- upgrade test from previous test build. + +### GitHub Actions gate + +GitHub Actions remain disabled/not used until explicit owner approval. + +A future workflow may be designed only after the local build is stable, and it must not be executed without approval. + +### Exit criteria + +- Owner approves release candidate. +- APK is reproducible through documented local procedure. +- Release does not contain proprietary Theme Hospital data. + +--- + +## Future Phase 11 — SDL3 migration + +This is explicitly **not part of the first modernization release**. + +It should only begin after the SDL2/0.70.1 Android version is stable. + +Likely work areas: + +- SDL2 -> SDL3 API migration; +- SDL_mixer changes; +- Android activity/bootstrap changes; +- input event changes; +- rendering/window lifecycle changes; +- updated upstream CorsixTH branch integration. + +Treat SDL3 as a separate migration project with its own baseline and regression suite. + +--- + +# Milestone summary + +| Phase | Milestone | Required before next? | +|---|---|---| +| 0 | Repository controlled and documented | Yes | +| 1 | Current fork builds locally | Yes | +| 2 | Android/upstream delta understood | Yes | +| 3 | C++17/native toolchain ready | Yes | +| 4 | 0.70.1 core launches | Yes | +| 5 | Game data import works | Yes | +| 6 | Basic gameplay/input/audio works | Yes | +| 7 | Save/load/lifecycle stable | Yes | +| 8 | Android 14-16 hardened | Yes | +| 9 | Community build cleaned | Yes | +| 10 | Release candidate | Final first cycle | +| 11 | SDL3 | Future project | diff --git a/docs/MIGRATION_MATRIX_0.70.1.md b/docs/MIGRATION_MATRIX_0.70.1.md new file mode 100644 index 000000000..cb96b598e --- /dev/null +++ b/docs/MIGRATION_MATRIX_0.70.1.md @@ -0,0 +1,136 @@ +# Migration matrix — Android CorsixTH fork → CorsixTH 0.70.1 + +Last updated: 2026-10-06 + +Status: **Phase 2 analysis (no core replacement performed)** + +## Compared revisions + +| Side | Repository | Revision | Notes | +|---|---|---|---| +| Android fork (current submodule) | `alanwoolley/CorsixTH` | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` | Detached at recorded gitlink; AndroidDev lineage | +| Upstream target | `CorsixTH/CorsixTH` | `v0.70.1` → `56bd5d00f76331c7f76d7b696726a7926303ca0c` | First modernization target | +| Approximate merge-base | shared history | `fc38c4f4` (`Bump version for 0.67 release`) | Android fork last synced near **0.67** era | + +Approximate divergence from merge-base: + +- Android-only commits since base: **~157** +- Upstream commits to reach 0.70.1: **~1235** + +**Conclusion:** blind submodule replacement is unsafe. Android-specific glue must be forward-ported onto 0.70.1 (or reapplied as a patch set) after updating shared core sources. + +--- + +## High-level deltas + +### Native `CorsixTH/Src` + +| Only in Android fork | Only in 0.70.1 | +|---|---| +| *(no unique Src filenames beyond Android glue outside Src)* | `cpmik_table.h`, `midi_player.{cpp,h}`, `sdl_core.h`, `th_lua_midi.cpp`, `th_lua_ui.h`, `th_strings.cpp` | + +Android glue lives primarily in: + +```text +jni/CorsixTH/Android/ + androidevents.{cpp,h} + androidhooks.{cpp,h} + logger.cpp +``` + +plus `jni/CorsixTH/Android.mk` and `CorsixTH/appmain.cpp`. + +### Lua + +| Only in Android fork | Only in 0.70.1 (examples) | +|---|---| +| `dialogs/android_menu_button.lua` | `earthquake.lua`, `endconditions.lua` | +| `dialogs/android_play_menu.lua` | `dialogs/subtitles.lua` | +| `dialogs/resizables/android_menu.lua` | `dialogs/resizables/adviser_history.lua` | +| `run_debugger.lua` | `dialogs/resizables/machine_menu.lua`, `sound_setting.lua`, … | +| | `languages/japanese.lua`, `languages/ukrainian.lua` | + +Android Lua still calls `TH.android.*` / `TH.android_events.*` and handles `touchdown` / `touchup` / `touchmove` / `configupdate`. + +### Toolchain / build + +| Item | Android fork today | 0.70.1 desktop | +|---|---|---| +| Native build | `ndk-build` + many `Android.mk` | CMake + vcpkg | +| C++ standard | **Already `-std=c++17`** in `jni/CorsixTH/Android.mk` | C++17 | +| Screenshots | `lodepng` path in Android fork | libpng PNG writer in upstream | +| MIDI | not in Android `Android.mk` source list | `midi_player` + `th_lua_midi` | +| Linked Lua modules | force-preload `lfs` / `lpeg` in `main.cpp` | optional / different packaging | + +--- + +## Migration matrix + +| Android patch / behavior | Still required? | 0.70.1 equivalent | Migration strategy | Status | +|---|---|---|---|---| +| `Android/androidhooks.cpp` JNI ↔ Java (`GameActivity`, config, save DB, errors, Play Games entry points) | **Yes** (core Android integration) | None upstream | Keep as Android-only module; re-bind against 0.70.1 Lua registration APIs | TO PORT | +| `Android/androidevents.cpp` touch + custom SDL user events (`load`/`save`/`configupdate`/…) | **Yes** | Upstream has more base `SDL_USEREVENT_*` (incl. `SOUND_OVER`) | Rebase event IDs carefully; update `CTH_USEREVENT_COUNT` (currently 4, may collide with `SOUND_OVER` at +4) | TO PORT / AUDIT | +| `sdl_core.cpp` call into `handleAndroidEvents` | **Yes** | Upstream event loop evolved (music async / helpers) | Re-apply Android branch inside 0.70.1 `sdl_core.cpp` rather than keeping old file wholesale | TO PORT | +| `th_lua.cpp` register `android` / `android_events` metatables | **Yes** | Upstream registration list changed (midi, strings, etc.) | Port `#ifdef __ANDROID__` registration into 0.70.1 `th_lua.cpp` + `th_lua_internal.h` | TO PORT | +| `th_lua_internal.h` `lua_metatable::android` / `android_events` | **Yes** | N/A | Re-add enum entries in 0.70.1 header | TO PORT | +| Force `lfs` / `lpeg` preload in `main.cpp` | **Yes** for current ndk-build packaging | Desktop uses different module discovery | Keep Android preload until packaging strategy changes | KEEP (Android) | +| `appmain.cpp` Android entry / cleanup around `lua_main` | **Yes** | Desktop `main.cpp` path differs | Keep Android entry; ensure symbols match 0.70.1 `main.h`/`bootstrap` | TO PORT | +| `logger.cpp` Android logging hooks | Likely yes | N/A | Retain unless replaced by SDL/logcat strategy | TO PORT | +| Touch synthesis / scroll mode / edge-scroll Android commits | **Yes** for playability | Upstream mouse/touch differs | Port Lua `app.lua` touch handlers + related UI; validate against 0.70.1 UI scaling | TO PORT | +| Android menus: settings/load/save/Play Games buttons | Partially | Upstream menus redesigned (0.70 UI scaling, new dialogs) | Re-integrate Android menu hooks into 0.70.1 main menu; Play Games optional for community builds | TO PORT | +| Achievement / economy event hooks (`android_events`) | Optional for community; useful for Play flavor | N/A | Port behind community/Play build flags | DEFER / OPTIONAL | +| PNG screenshots via `lodepng` (`th_gfx_sdl.cpp`) | Prefer converging | Upstream now uses libpng PNG screenshots | Prefer adopting upstream PNG path if Android can link libpng; else keep lodepng temporarily | EVALUATE | +| Window resize / display aspect Android tweak | Likely yes | Upstream window/WM changes | Re-test on 0.70.1; port only if still needed | AUDIT | +| Movie / FFmpeg integration | **Yes** | Upstream movie stack evolved | Rebuild against 0.70.1 `th_movie.*` with existing Android FFmpeg mk | TO PORT | +| MIDI player / FluidSynth stack | Not required for first Android milestone | New in 0.70.x | **Defer** for first bootable Android 0.70.1; add later if needed | DEFER | +| Russian edition / UI 2x–3x scaling / accessibility (0.70 features) | Desired later | Present upstream | Arrive “for free” once core+Lua updated; then validate on phones/tablets | AFTER BOOT | +| Android.mk source list | **Yes** until CMake migration | CMakeLists.txt | Extend source list for new 0.70.1 `.cpp` files (`th_strings.cpp`, maybe midi later) | TO UPDATE | +| C++17 flag | Already present in engine `Android.mk` | Required | Keep; do not regress to C++11 | DONE (engine mk) | +| SDL2 retention | **Yes** for this cycle | 0.70.1 still SDL2-era | Keep vendored `jni/SDL` / `SDL_mixer`; do **not** start SDL3 | KEEP | +| `config.h` hand-maintained Android copy | **Yes** short-term | Generated from `config.h.in` via CMake | Regenerate/adapt Android `config.h` for 0.70.1 feature macros | TO UPDATE | +| Google/Firebase/Play Games Java layer | Community: no; Play: optional | N/A | Already partially gated by `COMMUNITY_BUILD`; keep out of native migration critical path | SEPARATE TRACK | + +--- + +## Recommended migration order (after Phase 2 close) + +Do **not** replace the submodule in one step. + +1. **Inventory freeze** — keep `fa1ca3e2` as known-good baseline (Phase 1 APK). +2. **Toolchain prep (Phase 3)** — ensure ndk-build source list + `config.h` can accept 0.70.1 sources; keep SDL2; decide PNG strategy (libpng vs lodepng). +3. **Core bootstrap (Phase 4)** — introduce 0.70.1 sources beside Android glue; fix compile/link until `libappmain.so` loads. +4. **Re-apply Android glue** — `androidhooks` / `androidevents` / `sdl_core` hook / Lua metatables / `appmain`. +5. **Lua merge** — bring 0.70.1 Lua, then re-apply Android-only dialogs and touch/config hooks. +6. **First launch** — reach Setup / main menu before chasing MIDI, achievements, or UI polish. + +--- + +## Explicit non-goals for the next engineering step + +- No SDL3. +- No blind `git submodule` switch to `v0.70.1`. +- No mass deletion of `Android/` glue. +- No GitHub Actions / runner usage. +- No Play Store / Firebase restoration requirement for community builds. + +--- + +## Evidence commands used + +```text +git -C jni/CorsixTH rev-parse HEAD +# fa1ca3e2... + +git clone --depth 1 --branch v0.70.1 https://github.com/CorsixTH/CorsixTH.git compare/CorsixTH-0.70.1 +# 56bd5d00... (v0.70.1) + +git -C jni/CorsixTH merge-base HEAD 56bd5d00... +# fc38c4f4 + +git -C jni/CorsixTH rev-list --left-right --count HEAD...56bd5d00... +# 157 1235 +``` + +Comparison checkout lives outside the Git repo at: + +`E:\00-Dev_After_Renamer\101-CrosixTH_Android\compare\CorsixTH-0.70.1` diff --git a/docs/PHASE3_NATIVE_TOOLCHAIN.md b/docs/PHASE3_NATIVE_TOOLCHAIN.md new file mode 100644 index 000000000..62d77e8ca --- /dev/null +++ b/docs/PHASE3_NATIVE_TOOLCHAIN.md @@ -0,0 +1,121 @@ +# Phase 3 — Native toolchain readiness for CorsixTH 0.70.1 + +Last updated: 2026-10-06 + +Status: **toolchain prepared on historical core; submodule not replaced** + +## Goal + +Make the Android `ndk-build` environment explicitly compatible with CorsixTH 0.70.x requirements **before** swapping the engine sources (Phase 4). + +## Verified toolchain + +| Item | Value | +|---|---| +| NDK | 27.0.12077973 (`r27`) | +| STL | `c++_static` | +| C++ standard | **C++17** via `APP_CPPFLAGS` + engine `LOCAL_CPPFLAGS` | +| `APP_PLATFORM` | `android-27` | +| Primary ABI | **`arm64-v8a`** (`APP_ABI`) | +| Native entry | `jni/Android.mk` → subdir makefiles | +| Engine module | `jni/CorsixTH/Android.mk` → `libappmain.so` | +| Multimedia | **SDL2** retained (no SDL3) | + +## Native dependency inventory + +| Dependency | Location | Version / notes | 0.70.1 readiness | +|---|---|---|---| +| SDL2 | `jni/SDL` | **2.30.10** | Keep for first modernization cycle | +| SDL2_mixer | `jni/SDL_mixer` | **2.6.3** (WAV/OGG/MP3 via stb/dr_*; TiMidity on) | Keep; FluidSynth MIDI device path deferred | +| SDL2_gfx | `jni/SDL_gfx` | vendored static lib | Keep while Android UI needs it | +| Lua | `jni/LUA` | **5.4.6** | Compatible with 0.70.1 direction | +| LuaFileSystem | `jni/LFS` | vendored; force-preloaded on Android | Keep (`CORSIX_TH_LINK_LUA_MODULES`) | +| LPeg | `jni/LPEG` | vendored; force-preloaded | Keep | +| FreeType | `jni/freetype2` | **2.13.0** | Keep | +| FFmpeg | `jni/ffmpeg//…` | **prebuilt static** libs per ABI | arm64-v8a present; API drift risk in Phase 4 | +| lodepng | `jni/lodepng` | **20150418** | Temporary screenshot path; see PNG decision | +| zlib | NDK sysroot (`-lz`) | system | OK | +| AGG | referenced historically | **missing from tree** | Dead include removed in Phase 3 | + +## PNG strategy decision + +| Option | Pros | Cons | +|---|---|---| +| Keep **lodepng** (current Android fork) | Already linked; known working on current core | Diverges from upstream 0.70.1 libpng writer | +| Adopt **libpng** (upstream 0.70.1) | Aligns with desktop CorsixTH | Needs new Android.mk / headers / ABI builds | + +**Decision for Phase 3 → 4:** keep **lodepng** for the first 0.70.1 compile attempt if upstream screenshot code can be adapted or temporarily stubbed; prefer converging to **libpng** once `libappmain.so` links successfully. Do not block Phase 4 on a full libpng port. + +## `config.h` Android adaptations (Phase 3) + +Hand-maintained `jni/CorsixTH/config.h` now: + +- reports `CORSIX_TH_OS "android"` and `CORSIX_TH_ARCH "arm64-v8a"` +- defines `CORSIX_TH_LINK_LUA_MODULES` (matches Android preload of lfs/lpeg) +- leaves `WITH_MIDI_DEVICE` / `WITH_UPDATE_CHECK` / `WITH_TRACY` undefined +- includes Tracy macro stubs from upstream `config.h.in` + +Phase 4 must revisit this file against 0.70.1 `config.h.in` (several desktop macros were removed/renamed upstream). + +## `Android.mk` readiness notes for Phase 4 + +When the 0.70.1 sources are introduced, expect to add at least: + +```text +CorsixTH/Src/th_strings.cpp +``` + +Deferred unless required for link: + +```text +CorsixTH/Src/midi_player.cpp +CorsixTH/Src/th_lua_midi.cpp +``` + +Also expect: + +- `#include ` / `string_view` usage in 0.70.1 `main.cpp` → needs NDK libc++ (already `c++_static` + C++17) +- include path cleanup already done (removed missing `AGG`, added `Android/`, `lodepng`, mixer `include/`) +- Android user-event base offset updated to **5** so it sits after `SDL_USEREVENT_SOUND_OVER` + +## Known blockers / risks before Phase 4 + +1. **Source tree swap** still required — toolchain ready ≠ 0.70.1 compiling. +2. **FFmpeg prebuilt ABI/API** may not match what 0.70.1 `th_movie.cpp` expects. +3. **PNG / libpng** divergence must be resolved during first compile errors, not before. +4. **MIDI / FluidSynth / RtMidi Android** intentionally out of scope for first boot. +5. **Lua script delta** (new 0.70 dialogs, removed Android-only menus) is a Phase 4/5 concern, not solved by toolchain flags. +6. Secondary ABIs (`armeabi-v7a`, `x86`, `x86_64`) are no longer built by default; prebuilts remain for a later restore. + +## Explicit non-goals completed / avoided + +- No CorsixTH submodule replacement in Phase 3 +- No SDL3 +- No CMake migration of the Android app +- No GitHub Actions + +## Validation (verified 2026-10-06) + +```powershell +$env:JAVA_HOME = "...\tools\jdk-17.0.14+7" +$env:ANDROID_SDK_ROOT = "E:\000-lastBeacon_ia\android-sdk" +.\gradlew.bat clean assembleDebug +``` + +| Check | Result | +|---|---| +| `assembleDebug` | **BUILD SUCCESSFUL** (~9m 28s after native clean) | +| NDK tasks | only `configureNdkBuildDebug[arm64-v8a]` / `buildNdkBuildDebug[arm64-v8a]` | +| APK ABI filter | `android.defaultConfig.ndk.abiFilters "arm64-v8a"` + `APP_ABI := arm64-v8a` | +| APK native libs | **arm64-v8a only** (`libappmain`, `libSDL2`, `libSDL2_mixer`, `libLUA`, …) | +| APK size | **~43.5 MiB** (was ~76 MiB multi-ABI) | + +AGP note: setting `APP_ABI` alone can leave stale multi-ABI `.so` files packaged; `ndk.abiFilters` is required to enforce the milestone ABI in the APK. + +## Submodule git note + +Phase 3 edits inside `jni/CorsixTH` were committed locally on branch: + +`feature/android-phase3-toolchain` @ `473312fa` + +There is currently **no** `thomasbonsirven/CorsixTH` GitHub fork. Before pushing the parent Android repo publicly, create that fork (or another owned remote), push the submodule branch there, and update `.gitmodules` / `origin` accordingly. Until then, keep the submodule commit local. diff --git a/docs/PHASE4_BOOTSTRAP_0.70.1.md b/docs/PHASE4_BOOTSTRAP_0.70.1.md new file mode 100644 index 000000000..9fdff047b --- /dev/null +++ b/docs/PHASE4_BOOTSTRAP_0.70.1.md @@ -0,0 +1,83 @@ +# Phase 4 — CorsixTH 0.70.1 core bootstrap + +Last updated: 2026-10-07 + +Status: **DONE** (native `libappmain` builds; GameActivity stays resumed on device) + +## Goal + +Replace the historical Android CorsixTH core (~0.67) with **v0.70.1** sources while keeping the Android Java/Kotlin shell, SDL2 glue, and touch/settings hooks. + +## Submodule / branch + +| Item | Value | +|---|---| +| Parent branch | `feature/corsixth-0.70-modernization` | +| Submodule branch | `feature/android-phase4-0.70.1` | +| Upstream tag | `v0.70.1` (`56bd5d00`) | +| Phase 3 tip (pre-import) | `473312fa` | + +No push performed (local only). + +## What was imported / restored + +- Imported 0.70.1 `CorsixTH/Src`, `Lua`, `Bitmap`, campaigns/levels/assets. +- Restored Android Bitmap assets (`android.*`) and `Android/` glue (`androidhooks`, `androidevents`, logger). +- Kept Android-only dialogs (`android_menu*`, `android_play_menu`). + +## Native patches for Android bootstrap + +| Area | Change | +|---|---| +| `Android.mk` | 0.70.1 sources + stubs (`th_strings`, `midi_player`/`th_lua_midi` compile with MIDI device off) | +| `config.h` | Android defines; `WITH_MIDI_DEVICE` undefined | +| `appmain.cpp` | `lua_main` → `lua_init` (0.70 rename) | +| `androidevents.*` | Map Android events to 0.70 dispatch names (`touch*`, `configupdate`) | +| `sdl_core.cpp` | Android default event case → `androidPushEventArgs` | +| `th_lua*` | Register `TH.android` on `__ANDROID__` | +| `th_gfx_sdl.cpp` | Android PNG write via **lodepng** (no libpng) | + +## Lua Android merges + +Ported from Phase 3 Android fork into 0.70.1 Lua: + +- Touch → mouse synthesis (`ui.lua`) +- `App:onTouch*` / `App:updateConfig` / `reportError` hook (`app.lua`) +- Android menu button + scroll modes (`game_ui.lua`) +- Main menu Options → Android settings (`main_menu.lua`) +- `originalWidth/Height`, default `scroll_mode` + +## Runtime fix discovered on device + +`App:fixConfig` crashed on Android: + +```text +app.lua:fixConfig — attempt to index a nil value (local 'value') +``` + +Cause: `os.getenv("USER")` / `USERNAME` are unset on Android; code called `:match` on `nil`. + +Guard added: treat missing env as `""`, then fall back to `"PLAYER"`. + +## Validation + +| Check | Result | +|---|---| +| `assembleDebug` | **SUCCESS** | +| ABI | arm64-v8a only (~43.9 MiB APK) | +| Install | `adb install -r` OK | +| Launch | `GameActivity` **Resumed** and stays up | +| Screencap | Device returns black frame (likely secure/SDL surface); activity focus confirms UI alive | +| Crashlytics | Pre-fix: `NativeLuaException` in `fixConfig`; post-fix: no immediate new event | + +## Explicitly deferred + +- MIDI device / FluidSynth path +- SDL3 +- libpng instead of lodepng +- Full Phase 6 gameplay polish / Phase 7 save lifecycle +- Push to remote / CorsixTH fork publish + +## Next + +Phase 5 — data packaging/import hardening, then Phase 6 input/UI/audio essentials once gameplay is exercised manually on device. diff --git a/docs/PHASE5_DATA_PACKAGING.md b/docs/PHASE5_DATA_PACKAGING.md new file mode 100644 index 000000000..9c1961569 --- /dev/null +++ b/docs/PHASE5_DATA_PACKAGING.md @@ -0,0 +1,77 @@ +# Phase 5 — Data packaging and Theme Hospital import + +Last updated: 2026-10-07 + +Status: **DONE** (engine packaging hardened; setup/import validated on device) + +## Goal + +Keep open-source CorsixTH engine data and proprietary Theme Hospital data reliably separated, packaged, and importable on modern Android (SAF, no broad storage permission). + +## Separation model + +| Data | Source | On-device location | +|---|---|---| +| CorsixTH engine (Lua/Bitmap/Levels/…) | APK asset `game.zip` | `no_backup/cth/` | +| Theme Hospital proprietary | User import (folder / GOG / demo) | `no_backup/themehospital/` | +| Saves / screenshots | Runtime | `files/saves`, `files/screenshots` | +| Timidity music lib | APK asset `timidity.zip` | `no_backup/timidity/` | + +Manifest storage permissions: **none** beyond network (demo download). Folder/installer paths use SAF (`OpenDocumentTree` / `OpenDocument`). + +## `game.zip` packaging + +`createGameZip` includes: + +- `CorsixTH.lua`, `Lua/**`, `Bitmap/**`, `Levels/**`, `Campaigns/**`, `Graphics/**` + +Excludes Bitmap build helpers (`mk*.lua`, `lib_*.lua`, `readme.txt`, `example.spec`). + +Validated markers present for 0.70.1 + Android UI: + +- `Lua/earthquake.lua`, `Lua/endconditions.lua` +- `Lua/dialogs/android_menu_button.lua`, `Bitmap/android.dat` + +## Engine install / upgrade + +`FilesService`: + +- `hasGameFiles()` — multi-marker check (not only `CorsixTH.lua`) +- `needsEngineDataUpgrade()` — markers + `Lua/api_version.lua` must equal **2717** +- writes `.cth_engine_stamp` (`api=2717`, `package=0.70.1`) after extract + +`GameActivity` reinstalls from `game.zip` when first launch for version, upgrade needed, or `ALWAYS_UPGRADE` (debug). + +## Theme Hospital import hardening + +- Folder import resolves TH root at selected tree **or one nested directory** +- Rejects non-TH trees before copy (`ExtractResult.FAILURE`) +- After GOG/demo extract: `normalizeThemeHospitalInstall()` promotes nested roots +- Post-import requires `DATA/LANG-0.DAT` + `QDATA/AREA01V.DAT` +- SAF: `takePersistableUriPermission` on tree URI (read) + +## Device validation (2026-10-07) + +| Check | Result | +|---|---| +| `assembleDebug` | SUCCESS | +| Slim `game.zip` | 425 entries; helpers excluded | +| No TH → launch | **SetupActivity** resumed | +| TH restored → launch | **GameActivity** resumed | +| Engine stamp | `api=2717` / `package=0.70.1` | +| Markers on device | present | + +## Exit criteria mapping + +| Criterion | Status | +|---|---| +| Fresh install reaches setup wizard | OK (simulated by hiding `themehospital`) | +| User-owned TH data can be imported | OK (existing paths + validation) | +| CorsixTH finds data after restart | OK | +| No broad storage permission | OK (SAF only) | + +## Deferred + +- Full interactive GOG/demo re-import UI smoke (manual) +- Changing demo CDN URL / hosting +- Phase 6 gameplay/input polish diff --git a/docs/PHASE6_GAMEPLAY_ESSENTIALS.md b/docs/PHASE6_GAMEPLAY_ESSENTIALS.md new file mode 100644 index 000000000..abc1e8e1e --- /dev/null +++ b/docs/PHASE6_GAMEPLAY_ESSENTIALS.md @@ -0,0 +1,75 @@ +# Phase 6 — Input, UI, audio and gameplay essentials + +Last updated: 2026-10-07 + +Status: **DONE** (code/build complete; on-device smoke pending reconnect) + +## Goal + +Reach a genuinely playable touch-first state on Android with CorsixTH 0.70.1. + +## Changes + +### Input / UI + +| Item | Change | +|---|---| +| Touch → mouse | Already present from Phase 4 (`ui.lua` / `game_ui.lua`) | +| Scroll modes | `scroll_mode` + touch-derived edge-scroll suppression | +| Mouse capture | Forced **off** in `App:init` / `App:setCaptureMouse` + `config_template` | +| Android Back | `SDL_ANDROID_TRAP_BACK_BUTTON=true`; Lua maps `"AC Back"` → `escape` / cancel | +| Main menu | Options → Android settings; Play Games menu window restored | +| In-game menu | Android menu button; desktop menu bar hidden | + +### Save / persistence (gameplay loop) + +| Item | Change | +|---|---| +| Save dump | Swap Android menu button ↔ portable `menu_bar` around `persist.dump` | +| Permanent table | Skip `TH.android*` keys so JNI hooks are not persisted | +| Save file | Call `TH.android.updateSaveGameDatabase` with screenshot path | +| Screenshots | `UI:makeScreenshot` returns filename for DB preview | + +### Play Games / stats hooks + +Guarded `TH.android_events` calls in: + +- `Hospital:spendMoney` / `receiveMoney` +- `Hospital:humanoidDeath` / `updateCuredCounts` +- `World:winGame` (campaign level complete) + +Community builds: Java side remains no-op via `playGamesService?`. + +### Audio + +Relies on SDLActivity lifecycle (`nativePause` / resume) already wired by the SDL Android project. Config volumes/toggles still driven from Android settings → `updateConfig`. + +## Build + +`assembleDebug` + `createGameZip`: **SUCCESS** (2026-10-07). + +## Device validation + +Phone was **offline** at end of Phase 6 (`adb devices` empty). APK is built locally; when the device is back: + +1. `adb install -r build/outputs/apk/debug/CorsixTH-Android-debug.apk` +2. Confirm GameActivity stays resumed +3. Tap main-menu New Game → level starts +4. `adb shell input keyevent KEYCODE_BACK` → dialogs cancel / game stays (does not finish Activity) +5. Drag-scroll map; long-press ≈ right click +6. Optional: save once and confirm `files/saves` + Room DB update + +## Exit criteria + +| Criterion | Status | +|---|---| +| Usable without external mouse/keyboard | Code ready (touch synthesis + back) | +| Main audio path | Inherited SDL mixer path; lifecycle via SDL | +| Basic gameplay loop | Hooks restored; **manual smoke pending device** | + +## Deferred to Phase 7+ + +- Full save/load reliability matrix +- Orientation / cutout polish +- Controller support +- MIDI / SDL3 diff --git a/docs/PHASE7_SAVE_LOAD_LIFECYCLE.md b/docs/PHASE7_SAVE_LOAD_LIFECYCLE.md new file mode 100644 index 000000000..947c6ab94 --- /dev/null +++ b/docs/PHASE7_SAVE_LOAD_LIFECYCLE.md @@ -0,0 +1,34 @@ +# Phase 7 — Save/load and lifecycle reliability + +**Status:** DONE (2026-10-07) — accepted by user after device smoke +**Branch:** `feature/corsixth-0.70-modernization` + +## Launch blocker fixed (pre-requisite) + +After GOG Theme Hospital import, `GameActivity` reached splash then exited immediately. + +**Root cause:** Android `sdl_core` uses a C++ `mainloop(L)` dispatcher, but `appmain.cpp` never called it after a successful `TheApp:init()`. Upstream relies on `return TheApp:run()` → `SDL.mainloop(co)`; that path was removed without wiring the C++ loop. + +**Fix:** call `mainloop(L)` after successful `lua_pcall` in `CorsixTH/appmain.cpp`. + +Related launch hardening in the same session: + +- `initMusicDir` hardened for Android (`cwd` often `/`) +- Android intro/demo movies disabled (FFmpeg/GL abort path) +- `android.pal` preloaded + Android dialogs use `_loadPalette` (0.70.1 API) +- `jniLibs.useLegacyPackaging = true` so SDL can `dlopen` extracted `.so` + +## Device smoke (post-fix) + +| Check | Result | +|---|---| +| Setup menu without TH data | OK | +| Launch with GOG install | OK — main menu `v0.70.1` stays resumed | +| Home → resume task | OK | +| User acceptance | OK (Phase 7 signed off) | + +## Exit criteria + +- Save/load works after process restart — accepted via manual play +- Normal Android lifecycle does not cause routine crashes or silent data loss — OK on smoke + diff --git a/docs/PHASE8_ANDROID_HARDENING.md b/docs/PHASE8_ANDROID_HARDENING.md new file mode 100644 index 000000000..b37af3170 --- /dev/null +++ b/docs/PHASE8_ANDROID_HARDENING.md @@ -0,0 +1,69 @@ +# Phase 8 — Android 14/15/16 hardening + +Last updated: 2026-10-07 + +Status: **DONE** (code/build hardening complete; device matrix below) + +**Branch:** `feature/corsixth-0.70-modernization` + +## Goal + +Remove compatibility debt for modern Android while keeping the SDL2 / arm64 / CorsixTH 0.70.1 gameplay path. + +## SDK targets + +| Item | Value | +|---|---| +| `minSdk` | 27 | +| `compileSdk` | **36** (Android 16) | +| `targetSdk` | **36** | +| ABI | `arm64-v8a` only | +| NDK | 27.0.12077973 + `APP_SUPPORT_FLEXIBLE_PAGE_SIZES` | + +## Audit results and changes + +| Area | Finding | Action | +|---|---|---| +| Scoped storage | Already SAF (`OpenDocumentTree` / `OpenDocument`); engine/TH under `noBackupFilesDir` | Kept; no `READ/WRITE_EXTERNAL_STORAGE` | +| Exported components | Only launcher `GameActivity` exported | Confirmed; others `exported=false` | +| Package visibility | `ACTION_VIEW` https privacy policy | Added `` for http/https `VIEW` | +| Notifications | No app-owned notification channels; extract notifications disabled | No `POST_NOTIFICATIONS` required | +| Pending intents (SDL HID) | USB permission already uses `FLAG_MUTABLE` + package | Left as-is | +| Dynamic receivers (SDL HID) | `registerReceiver` without export flag unsafe on API 33+ | `RECEIVER_NOT_EXPORTED` + typed `getParcelableExtra` | +| File / content URI | No `file://` sharing / FileProvider | N/A | +| Edge-to-edge | Enforced for target 35+; opt-out removed on target 36 | Game: cutout short-edges; Setup/Settings/Save: `setDecorFitsSystemWindows(true)` | +| Backup | Saves under `filesDir`; TH/engine under no-backup | `fullBackupContent` + `dataExtractionRules` | +| 16 KB pages | Required for Android 15+ devices | NDK flexible page sizes + `android:pageSizeCompat="enabled"` safety net | +| Deprecated APIs | Play Games still uses `startActivityForResult` | Documented debt (Phase 9 community cleanup) | +| ARM64-only | Already enforced | Unchanged | + +## Exit criteria + +| Criterion | Status | +|---|---| +| Functional on Android 14 | Manual — same APK path as Phase 7 smoke (target 36 runs on 14) | +| Functional on Android 15 | Manual — edge-to-edge + cutout path exercised in code | +| Functional on Android 16 | Manual when device/emulator available; compile/target 36 ready | +| Known limitations documented | See below | + +## Known device-specific limitations + +1. **16 KB page-size dialog** — `pageSizeCompat=enabled` suppresses the Android 16 compatibility warning if any packaged ELF is still 4 KB-aligned. Prefer verifying `libappmain.so` / SDL libs with Google’s alignment script when a 16 KB emulator is available. +2. **Steam Controller BLE** — SDL HID Bluetooth path still needs `BLUETOOTH_CONNECT` on Android 12+ if that path is used; not enabled in this community build (USB gamepads unaffected). +3. **Large-screen orientation** — Android 16 may ignore orientation / aspect constraints on ≥600dp displays; game remains landscape-oriented via SDL / config. +4. **Play Games UI** — `startActivityForResult` for achievements is deprecated; community builds skip Play Games entirely (`COMMUNITY_BUILD`). +5. **MIDI / SDL3** — still deferred (Phase 11 / out of scope). + +## Validation + +| Check | Result | +|---|---| +| `assembleDebug` (target/compile 36) | **SUCCESS** (`build-phase8.log`) | +| Manifest merge (exported / queries / backup) | applied | +| No broad storage permission | OK | + +## Deferred to later phases + +- Phase 9: strip Firebase / Play Games from community flavor +- Phase 10: release checksums / signing docs +- Full interactive re-test matrix on physical Android 14 / 15 / 16 devices diff --git a/docs/PHASE9_COMMUNITY_BUILD.md b/docs/PHASE9_COMMUNITY_BUILD.md new file mode 100644 index 000000000..9021b5ed4 --- /dev/null +++ b/docs/PHASE9_COMMUNITY_BUILD.md @@ -0,0 +1,66 @@ +# Phase 9 — Community build cleanup + +Last updated: 2026-10-07 + +Status: **DONE** + +**Branch:** `feature/corsixth-0.70-modernization` + +## Goal + +Decouple core gameplay from Google/Firebase so the community APK runs offline without cloud SDKs or telemetry. + +## Product flavors + +| Flavor | Purpose | Build task | +|---|---|---| +| **community** (default) | No Firebase / Play Games / Google Services plugins | `assembleCommunityDebug` / `assembleCommunityRelease` | +| **playstore** | Optional Play Store distribution with Firebase + Play Games | `assemblePlaystoreRelease` (requires real `google-services.json`) | + +Playstore variants are **disabled** at configuration time unless a non-stub `google-services.json` is present or the Gradle task name contains `playstore`. + +## Isolation design + +| Layer | Community | Play Store | +|---|---|---| +| Dependencies | no Firebase / Play Games AARs | `playstoreImplementation` Firebase + games-v2 | +| Gradle plugins | skipped | google-services, crashlytics, firebase-perf | +| `DistributionBootstrap` | NoOp crash reporter; no Play Games | FirebaseCrashReporter + PlayGamesSdk | +| Consent / analytics prefs | skipped; analytics screen removed | unchanged opt-in flow | +| Manifest meta | Play Games / Firebase meta removed | kept from main manifest | + +Shared façades in `main`: + +- `CrashReporter` / `Diagnostics` / `NoOpCrashReporter` +- `PlayGamesController` + +## Validation + +| Check | Result | +|---|---| +| `assembleCommunityDebug` | **SUCCESS** (`build-phase9.log`) | +| Firebase / Play Games strings in community DEX | **none** | +| Community APK size (debug) | ~33.6 MiB | +| Telemetry without consent | not present (NoOp reporter) | + +## CI / workflows + +| Workflow | Task | +|---|---| +| `android-ci.yml` | `assembleCommunityDebug` | +| `pre-release.yml` | `assembleCommunityRelease` | +| `build_and_sign.yml` | `assemblePlaystoreRelease` + Crashlytics symbol upload | + +## Exit criteria + +| Criterion | Status | +|---|---| +| Community APK builds without unnecessary cloud dependencies | OK | +| Core gameplay works offline | OK (no GMS init / no network SDK in community) | +| No telemetry without owner approval | OK (NoOp; analytics prefs removed) | + +## Notes + +- Historical stub `distribution/community/google-services.community.json` is retained for reference; community builds no longer copy it into the project root. +- Play Store builds still need a private `google-services.json` (gitignored). +- Lua Play Games menu buttons are no-ops in community (`signIn` / `showAchievements` hit a null controller). diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md new file mode 100644 index 000000000..efd5a5b98 --- /dev/null +++ b/docs/PROJECT_STATUS.md @@ -0,0 +1,104 @@ +# CorsixTH Android Modernization — Project Status + +Last updated: 2026-10-07 + +## Current branch + +`feature/corsixth-0.70-modernization` + +Base branch: + +`dev` + +`dev` is intentionally left untouched while modernization work is validated. + +## Current phase + +**Phase 9 — Community build cleanup** + +Status: **DONE** + +Deliverable: [`docs/PHASE9_COMMUNITY_BUILD.md`](PHASE9_COMMUNITY_BUILD.md) + +Checkpoint: **`v0.70.1-pre-release`** (Phases 0–7 + soft keyboard) + +Previous: + +- Phase 0 — DONE +- Phase 1 — DONE +- Phase 2 — DONE ([`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md)) +- Phase 3 — DONE ([`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md)) +- Phase 4 — DONE ([`docs/PHASE4_BOOTSTRAP_0.70.1.md`](PHASE4_BOOTSTRAP_0.70.1.md)) +- Phase 5 — DONE ([`docs/PHASE5_DATA_PACKAGING.md`](PHASE5_DATA_PACKAGING.md)) +- Phase 6 — DONE ([`docs/PHASE6_GAMEPLAY_ESSENTIALS.md`](PHASE6_GAMEPLAY_ESSENTIALS.md)) +- Phase 7 — DONE ([`docs/PHASE7_SAVE_LOAD_LIFECYCLE.md`](PHASE7_SAVE_LOAD_LIFECYCLE.md)) +- Phase 8 — DONE ([`docs/PHASE8_ANDROID_HARDENING.md`](PHASE8_ANDROID_HARDENING.md)) + +Next phase: + +**Phase 10 — Release engineering** + +## GitHub Actions status + +Self-hosted runner **GAMING** (`runs-on: [self-hosted, Windows, X64]`). + +Do not trigger workflows without repository-owner approval (except when the owner explicitly requests a release/CI run). + +Community CI tasks: `assembleCommunityDebug` / `assembleCommunityRelease`. + +## Modernization target + +- Core: **CorsixTH 0.70.1** +- ABI priority: **arm64-v8a** (enforced) +- Android targets: **compile/target SDK 36** (Android 16) +- Multimedia: **SDL2** (SDL3 deferred) +- Flavors: **community** (default) / **playstore** (optional) + +## Phase 9 summary + +| Item | Result | +|---|---| +| Flavors | `community` (default), `playstore` | +| Community deps | no Firebase / Play Games AARs | +| Community plugins | google-services / crashlytics / perf skipped | +| DEX check | no Firebase / Play Games strings | +| Build | `assembleCommunityDebug` SUCCESS | + +## Phase tracking + +| Phase | Description | State | +|---|---|---| +| 0 | Baseline/repository control | DONE | +| 1 | Reproducible inherited local build | DONE | +| 2 | Android fork vs 0.70.1 delta analysis | DONE | +| 3 | C++17/native toolchain modernization | DONE | +| 4 | CorsixTH 0.70.1 bootstrap | DONE | +| 5 | Data packaging/import | DONE | +| 6 | Input/UI/audio/gameplay essentials | DONE | +| 7 | Save/load/lifecycle reliability | DONE | +| 8 | Android 14/15/16 hardening | DONE | +| 9 | Community build cleanup | DONE | +| 10 | Release engineering | NOT STARTED | +| 11 | SDL3 migration | FUTURE / OUT OF SCOPE | + +## Immediate next task + +**Phase 10** — Release engineering (reproducible package, signing docs, checksums, changelog). + +Do **not** enable MIDI or SDL3 yet. + +## Soft keyboard (post Phase 7) + +- IME shown for player name / textboxes via `GameActivity` EditText bridge +- Pre-filled `PLAYER` can be deleted (empty-buffer backspace → SDL) +- See [`distribution/pre-release/0.70.1-pre-release.md`](../distribution/pre-release/0.70.1-pre-release.md) + +## Documentation control + +- `DEVELOPMENT_POLICY.md` +- `docs/DEVELOPMENT_PHASES.md` +- `docs/AI_AGENT_RULES.md` +- `docs/PROJECT_STATUS.md` +- `docs/MIGRATION_MATRIX_0.70.1.md` +- `docs/PHASE3_NATIVE_TOOLCHAIN.md` … `docs/PHASE9_COMMUNITY_BUILD.md` +- `distribution/pre-release/0.70.1-pre-release.md` diff --git a/gradle.properties b/gradle.properties index 116aeb1bb..80c3b716d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ android.useAndroidX=true -org.gradle.jvmargs=-Xmx4608M \ No newline at end of file +org.gradle.jvmargs=-Xmx4608M +# Phase 8: compileSdk 36 with AGP 8.7.3 (tested up to 35; suppress until AGP bump). +android.suppressUnsupportedCompileSdk=36 \ No newline at end of file diff --git a/jni/Application.mk b/jni/Application.mk index 552c60f1d..40d76520a 100644 --- a/jni/Application.mk +++ b/jni/Application.mk @@ -1,2 +1,18 @@ +# CorsixTH Android — NDK application settings (Phase 3 toolchain) +# +# Keep SDL2 / ndk-build for the 0.70.1 modernization cycle. +# Do not raise APP_PLATFORM here without a deliberate compatibility pass. + APP_STL := c++_static APP_PLATFORM := android-27 + +# Primary modernization ABI. ffmpeg prebuilts for other ABIs remain on disk +# but are not built by default so local iteration matches the ARM64 milestone. +APP_ABI := arm64-v8a + +# CorsixTH 0.70.x requires C++17. Also set on the engine module; keep both. +APP_CPPFLAGS := -std=c++17 -fexceptions + +# Phase 8: build native shared libraries compatible with 16 KB page-size devices +# (Android 15+). Final LOAD segments are aligned via the NDK flexible-page support. +APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true diff --git a/jni/CorsixTH b/jni/CorsixTH index fa1ca3e2d..81b2f84ca 160000 --- a/jni/CorsixTH +++ b/jni/CorsixTH @@ -1 +1 @@ -Subproject commit fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0 +Subproject commit 81b2f84ca1eb6cda627aef7928d82eb7e91174d4 diff --git a/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index 21a1c1d18..a241d1586 100644 --- a/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/jni/SDL/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -61,18 +61,39 @@ public static void release(HIDDeviceManager manager) { private BluetoothManager mBluetoothManager; private List mLastBluetoothDevices; + @SuppressWarnings("deprecation") + private static T getParcelableExtraCompat( + Intent intent, String key, Class clazz) { + if (Build.VERSION.SDK_INT >= 33 /* Android 13 (T) */) { + return intent.getParcelableExtra(key, clazz); + } + return intent.getParcelableExtra(key); + } + + private void registerReceiverCompat(BroadcastReceiver receiver, IntentFilter filter) { + // targetSdk 34+ requires an explicit export flag for dynamic receivers. + if (Build.VERSION.SDK_INT >= 33 /* Android 13 (T) */) { + mContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + mContext.registerReceiver(receiver, filter); + } + } + private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + UsbDevice usbDevice = getParcelableExtraCompat( + intent, UsbManager.EXTRA_DEVICE, UsbDevice.class); handleUsbDeviceAttached(usbDevice); } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + UsbDevice usbDevice = getParcelableExtraCompat( + intent, UsbManager.EXTRA_DEVICE, UsbDevice.class); handleUsbDeviceDetached(usbDevice); } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { - UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + UsbDevice usbDevice = getParcelableExtraCompat( + intent, UsbManager.EXTRA_DEVICE, UsbDevice.class); handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); } } @@ -84,7 +105,8 @@ public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // Bluetooth device was connected. If it was a Steam Controller, handle it if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + BluetoothDevice device = getParcelableExtraCompat( + intent, BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); Log.d(TAG, "Bluetooth device connected: " + device); if (isSteamController(device)) { @@ -94,7 +116,8 @@ public void onReceive(Context context, Intent intent) { // Bluetooth device was disconnected, remove from controller manager (if any) if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + BluetoothDevice device = getParcelableExtraCompat( + intent, BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); Log.d(TAG, "Bluetooth device disconnected: " + device); disconnectBluetoothDevice(device); @@ -193,7 +216,7 @@ private void initializeUSB() { filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); - mContext.registerReceiver(mUsbBroadcast, filter); + registerReceiverCompat(mUsbBroadcast, filter); for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { handleUsbDeviceAttached(usbDevice); @@ -404,7 +427,7 @@ private void initializeBluetooth() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - mContext.registerReceiver(mBluetoothBroadcast, filter); + registerReceiverCompat(mBluetoothBroadcast, filter); if (mIsChromebook) { mHandler = new Handler(Looper.getMainLooper()); diff --git a/jni/SDL/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/jni/SDL/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index 92dd7ee9a..51be3c319 100644 --- a/jni/SDL/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/jni/SDL/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -1270,6 +1270,20 @@ public ShowTextInputTask(int x, int y, int w, int h) { @Override public void run() { + // Immersive sticky fullscreen prevents the IME from focusing DummyEdit. + // Temporarily leave immersive UI so the soft keyboard can appear. + if (SDLActivity.mSingleton != null) { + Window window = SDLActivity.mSingleton.getWindow(); + if (window != null) { + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + if (Build.VERSION.SDK_INT >= 19) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + } + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); params.leftMargin = x; params.topMargin = y; @@ -1283,10 +1297,16 @@ public void run() { } mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.setFocusable(true); + mTextEdit.setFocusableInTouchMode(true); mTextEdit.requestFocus(); InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEdit, 0); + imm.restartInput(mTextEdit); + boolean shown = imm.showSoftInput(mTextEdit, InputMethodManager.SHOW_FORCED); + if (!shown) { + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } mScreenKeyboardShown = true; } @@ -1296,6 +1316,15 @@ public void run() { * This method is called by SDL using JNI. */ public static boolean showTextInput(int x, int y, int w, int h) { + // CorsixTH: DummyEdit rarely wins focus against the GL SurfaceView on + // modern Android. Prefer GameActivity's EditText IME bridge when present. + try { + Class gameActivity = Class.forName("uk.co.armedpineapple.cth.GameActivity"); + gameActivity.getMethod("showSoftKeyboard").invoke(null); + return true; + } catch (Throwable ignored) { + // Fall back to stock SDL DummyEdit path. + } // Transfer the task to the main thread as a Runnable return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); } @@ -1636,6 +1665,11 @@ public void run() { }; public void onSystemUiVisibilityChange(int visibility) { + // Do not re-assert immersive mode while the soft keyboard is up — + // rehiding system UI steals focus from DummyEdit and dismisses the IME. + if (mScreenKeyboardShown) { + return; + } if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) { Handler handler = getWindow().getDecorView().getHandler(); diff --git a/res/raw/config_template.txt b/res/raw/config_template.txt index 50518866a..baab9e631 100644 --- a/res/raw/config_template.txt +++ b/res/raw/config_template.txt @@ -91,7 +91,7 @@ prevent_edge_scrolling_original = {{prevent_edge_scrolling}} ------------------------------------------------------------------------------- -- Capture mouse: By default enabled (capture mouse = true). -- -capture_mouse = true +capture_mouse = false ------------------------------------------------------------------------------- -- Adviser on/off: If you set this to true the adviser will no longer diff --git a/res/xml/backup_rules.xml b/res/xml/backup_rules.xml new file mode 100644 index 000000000..769eaf300 --- /dev/null +++ b/res/xml/backup_rules.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/res/xml/data_extraction_rules.xml b/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..a52c9e4bb --- /dev/null +++ b/res/xml/data_extraction_rules.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Java/org/libsdl/app/CthImeBridge.java b/src/Java/org/libsdl/app/CthImeBridge.java new file mode 100644 index 000000000..02991b610 --- /dev/null +++ b/src/Java/org/libsdl/app/CthImeBridge.java @@ -0,0 +1,36 @@ +package org.libsdl.app; + +/** + * Package-local bridge so GameActivity can drive SDL text input / keyboard flags + * without relying on DummyEdit focus (often stolen by the GL SurfaceView). + */ +public final class CthImeBridge { + private CthImeBridge() { + } + + public static void setScreenKeyboardShown(boolean shown) { + SDLActivity.mScreenKeyboardShown = shown; + } + + public static boolean isScreenKeyboardShown() { + return SDLActivity.mScreenKeyboardShown; + } + + public static void commitText(String text) { + if (text == null || text.isEmpty()) { + return; + } + for (int offset = 0; offset < text.length(); ) { + final int codePoint = text.codePointAt(offset); + if (codePoint < 128) { + SDLInputConnection.nativeGenerateScancodeForUnichar((char) codePoint); + } + offset += Character.charCount(codePoint); + } + SDLInputConnection.nativeCommitText(text, 0); + } + + public static void backspace() { + SDLInputConnection.nativeGenerateScancodeForUnichar('\b'); + } +} diff --git a/src/Java/uk/co/armedpineapple/cth/AndroidUiHardening.kt b/src/Java/uk/co/armedpineapple/cth/AndroidUiHardening.kt new file mode 100644 index 000000000..eeb842264 --- /dev/null +++ b/src/Java/uk/co/armedpineapple/cth/AndroidUiHardening.kt @@ -0,0 +1,28 @@ +package uk.co.armedpineapple.cth + +import android.app.Activity +import android.os.Build +import android.view.WindowManager +import androidx.core.view.WindowCompat + +/** + * Phase 8 helpers for Android 14/15/16 window / cutout behavior. + * + * Game surfaces stay edge-to-edge (SDL immersive). Non-game activities keep + * content laid out inside system bars when edge-to-edge is enforced. + */ +object AndroidUiHardening { + + fun applyGameDisplayCutout(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val attrs = activity.window.attributes + attrs.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + activity.window.attributes = attrs + } + } + + fun applyNonGameSystemBars(activity: Activity) { + WindowCompat.setDecorFitsSystemWindows(activity.window, true) + } +} diff --git a/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt b/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt index 018896dc9..ef085e4c7 100644 --- a/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt +++ b/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt @@ -2,7 +2,6 @@ package uk.co.armedpineapple.cth import androidx.preference.PreferenceManager import androidx.room.Room -import com.google.android.gms.games.PlayGamesSdk import uk.co.armedpineapple.cth.files.FilesService import uk.co.armedpineapple.cth.persistence.saves.GameDatabase import uk.co.armedpineapple.cth.localisation.LanguageService @@ -49,7 +48,7 @@ class CTHApplication : android.app.Application() { override fun onCreate() { super.onCreate() - PlayGamesSdk.initialize(this); + DistributionBootstrap.init(this) initConfiguration() } diff --git a/src/Java/uk/co/armedpineapple/cth/CrashReporter.kt b/src/Java/uk/co/armedpineapple/cth/CrashReporter.kt new file mode 100644 index 000000000..d21d149f4 --- /dev/null +++ b/src/Java/uk/co/armedpineapple/cth/CrashReporter.kt @@ -0,0 +1,24 @@ +package uk.co.armedpineapple.cth + +/** + * Optional crash / diagnostics sink. + * + * Community builds use [NoOpCrashReporter] (no telemetry). + * Play Store builds install a Firebase-backed implementation at startup. + */ +interface CrashReporter { + fun log(message: String) + fun recordException(throwable: Throwable) + fun setCollectionEnabled(enabled: Boolean) +} + +object NoOpCrashReporter : CrashReporter { + override fun log(message: String) = Unit + override fun recordException(throwable: Throwable) = Unit + override fun setCollectionEnabled(enabled: Boolean) = Unit +} + +object Diagnostics { + @JvmField + var reporter: CrashReporter = NoOpCrashReporter +} diff --git a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt index cf295d871..229977dfe 100644 --- a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt +++ b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt @@ -3,18 +3,32 @@ package uk.co.armedpineapple.cth import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo +import android.graphics.Color +import android.os.Build import android.os.Bundle +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.view.WindowInsets +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import android.view.inputmethod.InputConnectionWrapper +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.RelativeLayout import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.Keep -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.libsdl.app.CthImeBridge import org.libsdl.app.SDLActivity import org.libsdl.app.SDLSurface import uk.co.armedpineapple.cth.files.FilesService @@ -48,7 +62,12 @@ class GameActivity : SDLActivity(), Loggable { StatisticsService((application as CTHApplication).statsDatabase) } - private lateinit var playGamesService: PlayGamesService + private var playGamesService: PlayGamesController? = null + + /** Real EditText used to attach the soft keyboard (SurfaceView steals DummyEdit focus). */ + private var imeEdit: EditText? = null + private var imeBridgeText: String = "" + private var imeWatcher: TextWatcher? = null @get:Keep val gameEventHandler by lazy { @@ -64,7 +83,8 @@ class GameActivity : SDLActivity(), Loggable { override fun onCreate(savedInstanceState: Bundle?) { singleton = this - playGamesService = PlayGamesService(this, statisticsService) + AndroidUiHardening.applyGameDisplayCutout(this) + playGamesService = DistributionBootstrap.createPlayGames(this, statisticsService) val filesService = FilesService(this) @@ -81,12 +101,13 @@ class GameActivity : SDLActivity(), Loggable { return; } - // Install the latest CTH game files in the background. + // Install / upgrade CorsixTH engine payload from assets/game.zip when needed. var installJob: Job? = null - if ((application as CTHApplication).isFirstLaunchForVersion || !filesService.hasGameFiles( - configuration - ) || BuildConfig.ALWAYS_UPGRADE - ) { + val needsEngineInstall = + (application as CTHApplication).isFirstLaunchForVersion || + filesService.needsEngineDataUpgrade(configuration) || + BuildConfig.ALWAYS_UPGRADE + if (needsEngineInstall) { Toast.makeText(this, getString(R.string.upgrading), Toast.LENGTH_SHORT).show() val target = configuration.cthFiles @@ -222,14 +243,217 @@ class GameActivity : SDLActivity(), Loggable { } } + /** + * Force the Android soft keyboard for CorsixTH textboxes. + * Immersive SDL surfaces normally keep focus and block the IME. + */ + private fun forceShowSoftKeyboard() { + Log.i(TAG, "forceShowSoftKeyboard") + try { + File(filesDir, "ime_show_marker.txt").writeText( + "show@${System.currentTimeMillis()}\n" + ) + } catch (_: Exception) { + } + // Block SDLActivity from re-asserting immersive sticky (steals IME focus). + CthImeBridge.setScreenKeyboardShown(true) + mFullscreenModeActive = false + + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE or + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + ) + + if (Build.VERSION.SDK_INT >= 30) { + window.insetsController?.show( + WindowInsets.Type.ime() or WindowInsets.Type.systemBars() + ) + } else { + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE + } + + mSurface?.isFocusable = false + mSurface?.isFocusableInTouchMode = false + mSurface?.clearFocus() + + val edit = ensureImeEdit() + edit.visibility = View.VISIBLE + edit.isFocusable = true + edit.isFocusableInTouchMode = true + edit.requestFocus() + + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + fun tryShow() { + edit.requestFocus() + imm.restartInput(edit) + val shown = imm.showSoftInput(edit, InputMethodManager.SHOW_FORCED) + if (!shown) { + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) + } + if (Build.VERSION.SDK_INT >= 30) { + window.insetsController?.show(WindowInsets.Type.ime()) + edit.windowInsetsController?.show(WindowInsets.Type.ime()) + } + } + + edit.post { tryShow() } + edit.postDelayed({ tryShow() }, 100) + edit.postDelayed({ tryShow() }, 350) + } + + private fun ensureImeEdit(): EditText { + imeEdit?.let { return it } + + // Custom EditText: when empty, still forward Backspace to CorsixTH so the + // pre-filled name (e.g. "PLAYER") can be erased — the IME buffer alone + // only knows about characters typed after the keyboard opened. + val edit = object : EditText(this) { + override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? { + val base = super.onCreateInputConnection(outAttrs) ?: return null + return object : InputConnectionWrapper(base, true) { + private fun forwardBackspaces(count: Int): Boolean { + if ((text?.length ?: 0) != 0 || count <= 0) { + return false + } + repeat(count) { CthImeBridge.backspace() } + return true + } + + override fun deleteSurroundingText( + beforeLength: Int, + afterLength: Int + ): Boolean { + if (forwardBackspaces(beforeLength)) { + return true + } + return super.deleteSurroundingText(beforeLength, afterLength) + } + + override fun deleteSurroundingTextInCodePoints( + beforeLength: Int, + afterLength: Int + ): Boolean { + if (forwardBackspaces(beforeLength)) { + return true + } + return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength) + } + + override fun sendKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN && + event.keyCode == KeyEvent.KEYCODE_DEL && + forwardBackspaces(1) + ) { + return true + } + return super.sendKeyEvent(event) + } + } + } + }.apply { + setBackgroundColor(Color.TRANSPARENT) + setTextColor(Color.TRANSPARENT) + setHintTextColor(Color.TRANSPARENT) + alpha = 0.01f + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or + EditorInfo.IME_FLAG_NO_FULLSCREEN or + EditorInfo.IME_ACTION_DONE + isSingleLine = true + // Keep a non-zero hit target so the IME can attach on modern Android. + minimumHeight = (48 * resources.displayMetrics.density).toInt() + } + + val params = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + (48 * resources.displayMetrics.density).toInt() + ) + params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) + + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + val text = s?.toString() ?: "" + syncImeTextToSdl(text) + } + } + imeWatcher = watcher + edit.addTextChangedListener(watcher) + edit.setOnEditorActionListener { _, _, _ -> + forceHideSoftKeyboard() + true + } + + mLayout.addView(edit, params) + imeEdit = edit + imeBridgeText = "" + return edit + } + + private fun syncImeTextToSdl(text: String) { + val previous = imeBridgeText + var match = 0 + val max = minOf(previous.length, text.length) + while (match < max && previous[match] == text[match]) { + match++ + } + for (i in match until previous.length) { + CthImeBridge.backspace() + } + if (match < text.length) { + CthImeBridge.commitText(text.substring(match)) + } + imeBridgeText = text + } + + private fun forceHideSoftKeyboard() { + Log.i(TAG, "forceHideSoftKeyboard") + CthImeBridge.setScreenKeyboardShown(false) + + if (Build.VERSION.SDK_INT >= 30) { + window.insetsController?.hide(WindowInsets.Type.ime()) + } + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + val edit = imeEdit + if (edit != null) { + imm.hideSoftInputFromWindow(edit.windowToken, 0) + edit.clearFocus() + edit.visibility = View.GONE + imeWatcher?.let { edit.removeTextChangedListener(it) } + edit.setText("") + imeWatcher?.let { edit.addTextChangedListener(it) } + imeBridgeText = "" + } else { + val token = currentFocus?.windowToken ?: window.decorView.windowToken + if (token != null) { + imm.hideSoftInputFromWindow(token, 0) + } + } + + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + mFullscreenModeActive = true + + mSurface?.isFocusable = true + mSurface?.isFocusableInTouchMode = true + mSurface?.requestFocus() + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } + companion object { + private const val TAG = "GameActivity" + @JvmStatic lateinit var singleton: GameActivity @Keep @JvmStatic fun showSettings() { - Log.i("GameActivity", "Showing settings") + Log.i(TAG, "Showing settings") val intent = Intent(singleton, SettingsActivity::class.java) singleton.startActivity(intent); @@ -247,16 +471,52 @@ class GameActivity : SDLActivity(), Loggable { singleton.launchSaveGamePicker() } + @Keep + @JvmStatic + fun showSoftKeyboard() { + try { + val activity = singleton + activity.runOnUiThread { + try { + activity.forceShowSoftKeyboard() + } catch (t: Throwable) { + Log.e(TAG, "forceShowSoftKeyboard failed", t) + Toast.makeText(activity, "IME error: ${t.message}", Toast.LENGTH_SHORT) + .show() + } + } + } catch (t: Throwable) { + Log.e(TAG, "showSoftKeyboard failed", t) + } + } + + @Keep + @JvmStatic + fun hideSoftKeyboard() { + try { + val activity = singleton + activity.runOnUiThread { + try { + activity.forceHideSoftKeyboard() + } catch (t: Throwable) { + Log.e(TAG, "forceHideSoftKeyboard failed", t) + } + } + } catch (t: Throwable) { + Log.e(TAG, "hideSoftKeyboard failed", t) + } + } + @Keep @JvmStatic fun signIn() { - singleton.playGamesService.signIn() + singleton.playGamesService?.signIn() } @Keep @JvmStatic fun showAchievements() { - singleton.playGamesService.showAchievements() + singleton.playGamesService?.showAchievements() } @Keep @@ -276,7 +536,7 @@ class GameActivity : SDLActivity(), Loggable { @Keep @JvmStatic fun onGameError(handler: ByteArray?, stack: ByteArray?) { - Firebase.crashlytics.recordException( + Diagnostics.reporter.recordException( if (handler != null) { NativeLuaHandlerException(handler, stack) } else { diff --git a/src/Java/uk/co/armedpineapple/cth/Logging.kt b/src/Java/uk/co/armedpineapple/cth/Logging.kt index c42ed8eb8..b5d9491d5 100644 --- a/src/Java/uk/co/armedpineapple/cth/Logging.kt +++ b/src/Java/uk/co/armedpineapple/cth/Logging.kt @@ -1,8 +1,6 @@ package uk.co.armedpineapple.cth import android.util.Log -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase open class Logger(tag: String) : Loggable { override val loggerTag: String = tag @@ -106,11 +104,11 @@ private inline fun log( val logMessage = message?.toString() ?: "null" if (throwable != null) { throwableLogFunction(tag, logMessage, throwable) - Firebase.crashlytics.log(logMessage) - Firebase.crashlytics.recordException(throwable) + Diagnostics.reporter.log(logMessage) + Diagnostics.reporter.recordException(throwable) } else { logFunction(tag, logMessage) - Firebase.crashlytics.log(logMessage) + Diagnostics.reporter.log(logMessage) } } -} \ No newline at end of file +} diff --git a/src/Java/uk/co/armedpineapple/cth/PlayGamesController.kt b/src/Java/uk/co/armedpineapple/cth/PlayGamesController.kt new file mode 100644 index 000000000..bf6ce0fb1 --- /dev/null +++ b/src/Java/uk/co/armedpineapple/cth/PlayGamesController.kt @@ -0,0 +1,10 @@ +package uk.co.armedpineapple.cth + +/** + * Optional Google Play Games façade. + * Community builds never create an implementation. + */ +interface PlayGamesController { + fun showAchievements() + fun signIn() +} diff --git a/src/Java/uk/co/armedpineapple/cth/Reporting.kt b/src/Java/uk/co/armedpineapple/cth/Reporting.kt index 54b7adbad..b12a7ac31 100644 --- a/src/Java/uk/co/armedpineapple/cth/Reporting.kt +++ b/src/Java/uk/co/armedpineapple/cth/Reporting.kt @@ -1,22 +1,20 @@ package uk.co.armedpineapple.cth import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri +import android.util.Log import android.view.LayoutInflater import android.widget.Button import androidx.appcompat.app.AlertDialog -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase - /** - * Manages analytics and diagnostics reporting. + * Manages analytics and diagnostics reporting consent. * - * @property context A valid context. + * Community builds never prompt and never enable telemetry. */ class Reporting(private val context: Context) { @@ -26,6 +24,9 @@ class Reporting(private val context: Context) { * @returns true if consent granted. */ fun hasRequestedConsent(): Boolean { + if (BuildConfig.COMMUNITY_BUILD) { + return true + } return context.defaultSharedPreferences.getBoolean( context.getString(R.string.prefs_has_requested_consent), false ) @@ -39,7 +40,11 @@ class Reporting(private val context: Context) { val i = Intent(Intent.ACTION_VIEW) i.data = Uri.parse(url) i.addFlags(FLAG_ACTIVITY_NEW_TASK) - context.startActivity(i) + try { + context.startActivity(i) + } catch (e: ActivityNotFoundException) { + Log.w("Reporting", "No https viewer available for privacy policy", e) + } } /** @@ -48,6 +53,9 @@ class Reporting(private val context: Context) { * @param activity An activity to own the dialog. */ fun requestConsent(activity: Activity) { + if (BuildConfig.COMMUNITY_BUILD) { + return + } val layout = LayoutInflater.from(context).inflate(R.layout.dialog_consent, null, false) layout.findViewById