From 270c15ba17c527456d7f145122b9466bcc2e38ee Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 13:59:58 +0200 Subject: [PATCH 01/23] docs: add modernization development policy --- DEVELOPMENT_POLICY.md | 285 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 DEVELOPMENT_POLICY.md 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. From d911e6ab4c7eae6619950618c71ae90f8dd21aab Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 14:01:07 +0200 Subject: [PATCH 02/23] docs: define modernization development phases --- docs/DEVELOPMENT_PHASES.md | 422 +++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/DEVELOPMENT_PHASES.md 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 | From fa83ec0b23f4c7e910985dbd3c5f29f482d3b585 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 14:01:59 +0200 Subject: [PATCH 03/23] docs: add AI development agent rules --- docs/AI_AGENT_RULES.md | 270 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/AI_AGENT_RULES.md 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. From 4a1fd10deaecc8f77e60a82b92c8decf15131558 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 14:02:57 +0200 Subject: [PATCH 04/23] docs: add modernization project status --- docs/PROJECT_STATUS.md | 153 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/PROJECT_STATUS.md diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md new file mode 100644 index 000000000..19e5986fe --- /dev/null +++ b/docs/PROJECT_STATUS.md @@ -0,0 +1,153 @@ +# CorsixTH Android Modernization — Project Status + +Last initialized: 2026-10-06 + +## Current branch + +`feature/corsixth-0.70-modernization` + +Base branch: + +`dev` + +`dev` is intentionally left untouched while modernization work is validated. + +## Current phase + +**Phase 0 — Baseline and repository control** + +Status: **documentation/bootstrap in progress** + +Next phase after Phase 0 closeout: + +**Phase 1 — Reproducible local build of the inherited port on Windows** + +## GitHub Actions status + +**DO NOT RUN without explicit repository-owner approval.** + +The inherited workflows are retained as-is for reference only. + +Development validation is currently local-first. + +## Modernization target + +First target core: + +`CorsixTH 0.70.1` + +First target architecture: + +`arm64-v8a` + +Primary Android targets: + +- Android 16 +- Android 15 +- Android 14 + +SDL target for the first modernization cycle: + +`SDL2` + +SDL3 is deferred to a future independent phase. + +## Verified inherited repository state + +The following values are inherited from the fork and have been inspected in the modernization branch. + +| Component | Current inherited value | +|---|---| +| Default/base branch | `dev` | +| Android Gradle Plugin | `8.7.3` | +| Kotlin | `2.0.21` | +| KSP | `2.0.21-1.0.28` | +| Gradle wrapper | `8.9` | +| `minSdkVersion` | `27` | +| `targetSdkVersion` | `35` | +| `compileSdk` | `35` | +| Native build entry | `jni/Android.mk` via `ndkBuild` | +| STL | `c++_static` | +| NDK platform declaration | `android-27` | +| CorsixTH submodule URL | `https://github.com/alanwoolley/CorsixTH.git` | +| CorsixTH submodule branch hint | `Android` | +| CorsixTH submodule gitlink | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` | +| Firebase/Google plugins | Present in inherited build | +| Google Play Games | Present in inherited build | + +## Important inherited build facts + +The current Gradle build includes: + +- Google Services plugin; +- Firebase Crashlytics; +- Firebase Performance; +- Firebase Analytics; +- Google Play Games v2; +- Room/KSP; +- AndroidX; +- Kotlin/coroutines. + +These will **not** be removed during Phase 0/1 unless they prevent establishing the local baseline. They are scheduled for the later community-build cleanup phase. + +## Known architectural concern + +The Android app does not directly track upstream CorsixTH. It references the dedicated historical Android fork/branch: + +`alanwoolley/CorsixTH : Android` + +Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rather than a direct submodule replacement. + +## Phase tracking + +| Phase | Description | State | +|---|---|---| +| 0 | Baseline/repository control | IN PROGRESS | +| 1 | Reproducible inherited local build | NOT STARTED | +| 2 | Android fork vs 0.70.1 delta analysis | NOT STARTED | +| 3 | C++17/native toolchain modernization | NOT STARTED | +| 4 | CorsixTH 0.70.1 bootstrap | NOT STARTED | +| 5 | Data packaging/import | NOT STARTED | +| 6 | Input/UI/audio/gameplay essentials | NOT STARTED | +| 7 | Save/load/lifecycle reliability | NOT STARTED | +| 8 | Android 14/15/16 hardening | NOT STARTED | +| 9 | Community build cleanup | NOT STARTED | +| 10 | Release engineering | NOT STARTED | +| 11 | SDL3 migration | FUTURE / OUT OF SCOPE | + +## Phase 0 remaining checklist + +- [x] Create modernization branch. +- [x] Establish development policy. +- [x] Establish phase gates. +- [x] Establish AI/agent rules. +- [x] Confirm inherited GitHub Action branch triggers before making changes. +- [x] Record major Gradle/Android configuration. +- [x] Record CorsixTH submodule source and current gitlink. +- [ ] Inventory exact local JDK requirement through a clean local build. +- [ ] Determine/pin exact Android NDK version used for the successful baseline. +- [ ] Inventory all native dependency versions/sources. +- [ ] Compare historical Android core against upstream 0.70.1. + +The last item may transition directly into Phase 2 after Phase 1 establishes a known-good build. + +## Immediate next task + +**Do not migrate CorsixTH yet.** + +Next engineering task is to establish the inherited project as a reproducible local Windows build and record the exact toolchain that succeeds. + +Expected first local commands, after cloning with submodules and preparing required local configuration, will be based around the repository Gradle wrapper rather than a global Gradle installation. + +No workflow execution is required for Phase 1. + +## Documentation control + +Mandatory documents: + +- `DEVELOPMENT_POLICY.md` +- `docs/DEVELOPMENT_PHASES.md` +- `docs/AI_AGENT_RULES.md` +- `docs/PROJECT_STATUS.md` + +Any AI/developer continuing this work must read these files before implementation. From dba08e90d336f246b399e42c63e7fed9d3fab89f Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 15:36:19 +0200 Subject: [PATCH 05/23] docs: redefine fork purpose and modernization roadmap --- README.md | 360 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 335 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f6f2b2fdc..76a69e79e 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,359 @@ -[![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/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**. From e661a9f6a677376d9d46fad3e252213b69b3d234 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 23:27:44 +0200 Subject: [PATCH 06/23] fix(build): allow local debug build without google services Use a committed community stub when google-services.json is absent, and pin the NDK version verified by the Windows baseline. Co-authored-by: Cursor --- build.gradle | 20 +++++++++++++ .../community/google-services.community.json | 29 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 distribution/community/google-services.community.json diff --git a/build.gradle b/build.gradle index ab6124d9d..6272d78d6 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,23 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: 'versioning.gradle' apply plugin: 'kotlin-kapt' + +// Local/community baseline: allow assembleDebug without a private CI secret. +// If a real google-services.json is present (gitignored), it is used unchanged. +// Otherwise the committed community stub is copied into place for this build only. +def googleServicesJson = file('google-services.json') +def communityGoogleServicesStub = file('distribution/community/google-services.community.json') +if (!googleServicesJson.exists()) { + if (!communityGoogleServicesStub.exists()) { + throw new GradleException( + "google-services.json is missing and no community stub was found at " + + communityGoogleServicesStub.path) + } + googleServicesJson.text = communityGoogleServicesStub.text + logger.lifecycle("Using community google-services.json stub for local build " + + "(no private Google/Firebase credentials).") +} + apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.firebase-perf' @@ -77,6 +94,9 @@ 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 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" +} From 7ab3d3ccfeaaf47d33b438d9b2018b19a5928c2f Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 23:27:44 +0200 Subject: [PATCH 07/23] docs(build): document verified Windows assembleDebug baseline Record the toolchain, APK path/ABIs, google-services workaround, and Phase 1 partial status after a successful local build. Co-authored-by: Cursor --- docs/PROJECT_STATUS.md | 122 +++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 19e5986fe..2e6baf859 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # CorsixTH Android Modernization — Project Status -Last initialized: 2026-10-06 +Last updated: 2026-10-06 ## Current branch @@ -14,13 +14,13 @@ Base branch: ## Current phase -**Phase 0 — Baseline and repository control** +**Phase 1 — Reproducible local build of the inherited port** -Status: **documentation/bootstrap in progress** +Status: **PARTIAL — assembleDebug verified on Windows; device install not yet validated** -Next phase after Phase 0 closeout: +Previous: -**Phase 1 — Reproducible local build of the inherited port on Windows** +**Phase 0 — Baseline and repository control** — documentation established; local toolchain inventory completed via the Phase 1 build attempt. ## GitHub Actions status @@ -30,6 +30,8 @@ The inherited workflows are retained as-is for reference only. Development validation is currently local-first. +Self-hosted runner `GAMING` exists but must not be used without explicit approval. + ## Modernization target First target core: @@ -52,9 +54,65 @@ SDL target for the first modernization cycle: SDL3 is deferred to a future independent phase. -## Verified inherited repository state +## Verified Windows baseline (2026-10-06) + +The following values were **verified by executing** a local Windows build, not only by reading source. -The following values are inherited from the fork and have been inspected in the modernization branch. +| Item | Verified value | +|---|---| +| Host OS | Windows 11 Professionnel (`10.0.26200`) | +| Branch | `feature/corsixth-0.70-modernization` | +| HEAD at build time | `dba08e90d336f246b399e42c63e7fed9d3fab89f` (+ local Phase-1 build fixes) | +| CorsixTH submodule | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` (`alanwoolley/CorsixTH`, detached at Android fork revision) | +| JDK | Eclipse Temurin / OpenJDK **17.0.14+7** (portable local install) | +| Gradle wrapper | **8.9** | +| Android Gradle Plugin | **8.7.3** | +| Kotlin | **2.0.21** | +| Android SDK root used | `E:\000-lastBeacon_ia\android-sdk` | +| Platform | `platforms;android-35` | +| Build-Tools | `35.0.1` | +| Platform-Tools / ADB | `37.0.1` | +| NDK used | **27.0.12077973** (`r27`) | +| NDK path | `E:\000-lastBeacon_ia\android-sdk\ndk\27.0.12077973` | +| Native entry | `jni/Android.mk` via `ndkBuild` | +| STL / platform | `c++_static` / `android-27` | +| Command | `.\gradlew.bat assembleDebug --stacktrace` | +| Result | **BUILD SUCCESSFUL in 8m 43s** | +| APK path | `build/outputs/apk/debug/CorsixTH-Android-debug.apk` | +| APK size | **76.47 MiB** (80 179 730 bytes) | +| APK package | `uk.co.armedpineapple.cth` | +| APK versionName | `1.0.0-SNAPSHOT` | +| ABIs in APK | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` | +| Native libs observed | `libappmain.so`, `libSDL2.so`, `libSDL2_mixer.so`, `libLUA.so`, `libinnoextract.so`, Crashlytics NDK libs | +| Device test | **Not performed** — no authorized `adb devices` attached | + +### Environment notes + +- At session start, `JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and `java`/`adb` on PATH were unset. +- A partial Android SDK already existed at `E:\000-lastBeacon_ia\android-sdk` (platform 35 + platform-tools). +- Missing components installed with `sdkmanager`: `build-tools;35.0.1`, `ndk;27.0.12077973`. +- Portable JDK extracted under `E:\00-Dev_After_Renamer\101-CrosixTH_Android\tools\jdk-17.0.14+7` (outside the Git repository). +- Local `local.properties` points to the SDK root and is gitignored. + +### Build blockers encountered and fixes + +1. **Missing `google-services.json`** + - First root failure: `:processDebugGoogleServices` + - Minimal reversible fix: committed community stub at `distribution/community/google-services.community.json` and copy-into-place logic in `build.gradle` when the private file is absent. + - Private/real `google-services.json` remains gitignored and is preferred when present. + +2. **NDK selection not pinned in Gradle** + - Successful baseline used NDK **27.0.12077973**. + - `android.ndkVersion "27.0.12077973"` was added so future builds do not depend on deprecated `ndk.dir`. + +### Important warnings observed (non-fatal) + +- Deprecated `ASensorManager_getInstance` in SDL Android sensor code. +- `th_lua.cpp` non-void function missing return warning. +- Firebase Performance ASM instrumentation could not resolve some AndroidX/Window classes. +- Room `annotationProcessor` vs `kapt` configuration warning for `room-compiler`. + +## Inherited repository configuration (still accurate) | Component | Current inherited value | |---|---| @@ -72,24 +130,9 @@ The following values are inherited from the fork and have been inspected in the | CorsixTH submodule URL | `https://github.com/alanwoolley/CorsixTH.git` | | CorsixTH submodule branch hint | `Android` | | CorsixTH submodule gitlink | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` | -| Firebase/Google plugins | Present in inherited build | +| Firebase/Google plugins | Present in inherited build; community stub allows local compile | | Google Play Games | Present in inherited build | -## Important inherited build facts - -The current Gradle build includes: - -- Google Services plugin; -- Firebase Crashlytics; -- Firebase Performance; -- Firebase Analytics; -- Google Play Games v2; -- Room/KSP; -- AndroidX; -- Kotlin/coroutines. - -These will **not** be removed during Phase 0/1 unless they prevent establishing the local baseline. They are scheduled for the later community-build cleanup phase. - ## Known architectural concern The Android app does not directly track upstream CorsixTH. It references the dedicated historical Android fork/branch: @@ -102,8 +145,8 @@ Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rath | Phase | Description | State | |---|---|---| -| 0 | Baseline/repository control | IN PROGRESS | -| 1 | Reproducible inherited local build | NOT STARTED | +| 0 | Baseline/repository control | DONE (docs + toolchain inventory via Phase 1) | +| 1 | Reproducible inherited local build | PARTIAL (`assembleDebug` OK; device install pending) | | 2 | Android fork vs 0.70.1 delta analysis | NOT STARTED | | 3 | C++17/native toolchain modernization | NOT STARTED | | 4 | CorsixTH 0.70.1 bootstrap | NOT STARTED | @@ -115,29 +158,24 @@ Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rath | 10 | Release engineering | NOT STARTED | | 11 | SDL3 migration | FUTURE / OUT OF SCOPE | -## Phase 0 remaining checklist +## Phase 1 remaining checklist -- [x] Create modernization branch. -- [x] Establish development policy. -- [x] Establish phase gates. -- [x] Establish AI/agent rules. -- [x] Confirm inherited GitHub Action branch triggers before making changes. -- [x] Record major Gradle/Android configuration. -- [x] Record CorsixTH submodule source and current gitlink. -- [ ] Inventory exact local JDK requirement through a clean local build. -- [ ] Determine/pin exact Android NDK version used for the successful baseline. -- [ ] Inventory all native dependency versions/sources. -- [ ] Compare historical Android core against upstream 0.70.1. - -The last item may transition directly into Phase 2 after Phase 1 establishes a known-good build. +- [x] Initialize historical CorsixTH submodule at recorded revision. +- [x] Install/pin JDK 17 for AGP 8.7.3. +- [x] Use repository Gradle wrapper 8.9. +- [x] Install Android SDK Platform 35, Build-Tools 35.0.1, NDK 27.0.12077973. +- [x] Resolve missing `google-services.json` without committing secrets. +- [x] `assembleDebug` succeeds and produces an APK. +- [x] Record APK path, size and ABIs. +- [ ] Install APK on at least one ARM64 Android device via ADB. +- [ ] Confirm application reaches setup/first screen without immediate crash. +- [ ] Optional: document a one-command local env setup script (later, if useful). ## Immediate next task **Do not migrate CorsixTH yet.** -Next engineering task is to establish the inherited project as a reproducible local Windows build and record the exact toolchain that succeeds. - -Expected first local commands, after cloning with submodules and preparing required local configuration, will be based around the repository Gradle wrapper rather than a global Gradle installation. +Next engineering task: install the debug APK on an ARM64 Android device/emulator and confirm the first-launch/setup screen, then close Phase 1. No workflow execution is required for Phase 1. From 46725e1ef7f765e6ff91e2bf627216beb35b4b7d Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 23:49:34 +0200 Subject: [PATCH 08/23] fix(android): disable Firebase Perf and Play Games on community builds Community stub google-services.json has an invalid API key that crashes Firebase Performance within ~1s of launch. Skip Perf and Play Games init for that path so local debug can reach SetupActivity. Co-authored-by: Cursor --- build.gradle | 19 +++++++++++++++++-- .../co/armedpineapple/cth/CTHApplication.kt | 7 ++++++- .../uk/co/armedpineapple/cth/GameActivity.kt | 10 ++++++---- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 6272d78d6..36be0b896 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ apply plugin: 'kotlin-kapt' // Otherwise the committed community stub is copied into place for this build only. def googleServicesJson = file('google-services.json') def communityGoogleServicesStub = file('distribution/community/google-services.community.json') +def communityGoogleServicesMarker = 'corsixth-android-community-local' if (!googleServicesJson.exists()) { if (!communityGoogleServicesStub.exists()) { throw new GradleException( @@ -38,10 +39,20 @@ if (!googleServicesJson.exists()) { logger.lifecycle("Using community google-services.json stub for local build " + "(no private Google/Firebase credentials).") } +def usingCommunityGoogleServices = + googleServicesJson.exists() && + googleServicesJson.text.contains(communityGoogleServicesMarker) apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' -apply plugin: 'com.google.firebase.firebase-perf' +// Firebase Performance crashes at runtime with an invalid community API key +// (IllegalArgumentException in FirebaseInstallations / TransportManager). +// Keep it only when a real google-services.json is present. +if (!usingCommunityGoogleServices) { + apply plugin: 'com.google.firebase.firebase-perf' +} else { + logger.lifecycle("Community google-services stub detected: Firebase Performance disabled.") +} repositories { google() @@ -76,7 +87,9 @@ dependencies { 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' + if (!usingCommunityGoogleServices) { + implementation 'com.google.firebase:firebase-perf:21.0.3' + } implementation 'com.google.android.gms:play-services-games-v2:20.1.2' @@ -103,6 +116,8 @@ android { versionName = project.ext.buildVersion versionCode = project.ext.offsetBuildVersionCode + buildConfigField "boolean", "COMMUNITY_BUILD", + (usingCommunityGoogleServices ? "true" : "false") } compileOptions { diff --git a/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt b/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt index 018896dc9..4fa7852da 100644 --- a/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt +++ b/src/Java/uk/co/armedpineapple/cth/CTHApplication.kt @@ -49,7 +49,12 @@ class CTHApplication : android.app.Application() { override fun onCreate() { super.onCreate() - PlayGamesSdk.initialize(this); + // Community/debug builds use a stub google-services.json and are not + // linked to the Play Console project; initializing Play Games there only + // shows a useless sign-in UI and can confuse first-launch validation. + if (!BuildConfig.COMMUNITY_BUILD) { + PlayGamesSdk.initialize(this) + } initConfiguration() } diff --git a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt index cf295d871..a57c90e5e 100644 --- a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt +++ b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt @@ -48,7 +48,7 @@ class GameActivity : SDLActivity(), Loggable { StatisticsService((application as CTHApplication).statsDatabase) } - private lateinit var playGamesService: PlayGamesService + private var playGamesService: PlayGamesService? = null @get:Keep val gameEventHandler by lazy { @@ -64,7 +64,9 @@ class GameActivity : SDLActivity(), Loggable { override fun onCreate(savedInstanceState: Bundle?) { singleton = this - playGamesService = PlayGamesService(this, statisticsService) + if (!BuildConfig.COMMUNITY_BUILD) { + playGamesService = PlayGamesService(this, statisticsService) + } val filesService = FilesService(this) @@ -250,13 +252,13 @@ class GameActivity : SDLActivity(), Loggable { @Keep @JvmStatic fun signIn() { - singleton.playGamesService.signIn() + singleton.playGamesService?.signIn() } @Keep @JvmStatic fun showAchievements() { - singleton.playGamesService.showAchievements() + singleton.playGamesService?.showAchievements() } @Keep From 1bdfed356fcb92bb6c40a82f959d3d81174e3744 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 23:49:35 +0200 Subject: [PATCH 09/23] docs: record Firebase Perf community crash cause and device verification Co-authored-by: Cursor --- docs/PROJECT_STATUS.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 2e6baf859..c0f63f78d 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,4 +1,4 @@ -# CorsixTH Android Modernization — Project Status +# CorsixTH Android Modernization — Project Status Last updated: 2026-10-06 @@ -14,13 +14,13 @@ Base branch: ## Current phase -**Phase 1 — Reproducible local build of the inherited port** +**Phase 1 — Reproducible local build of the inherited port** -Status: **PARTIAL — assembleDebug verified on Windows; device install not yet validated** +Status: **PARTIAL — assembleDebug verified on Windows; device install not yet validated** Previous: -**Phase 0 — Baseline and repository control** — documentation established; local toolchain inventory completed via the Phase 1 build attempt. +**Phase 0 — Baseline and repository control** — documentation established; local toolchain inventory completed via the Phase 1 build attempt. ## GitHub Actions status @@ -79,12 +79,12 @@ The following values were **verified by executing** a local Windows build, not o | Command | `.\gradlew.bat assembleDebug --stacktrace` | | Result | **BUILD SUCCESSFUL in 8m 43s** | | APK path | `build/outputs/apk/debug/CorsixTH-Android-debug.apk` | -| APK size | **76.47 MiB** (80 179 730 bytes) | +| APK size | **76.47 MiB** (80 179 730 bytes) | | APK package | `uk.co.armedpineapple.cth` | | APK versionName | `1.0.0-SNAPSHOT` | | ABIs in APK | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` | | Native libs observed | `libappmain.so`, `libSDL2.so`, `libSDL2_mixer.so`, `libLUA.so`, `libinnoextract.so`, Crashlytics NDK libs | -| Device test | **Not performed** — no authorized `adb devices` attached | +| Device test | **Not performed** — no authorized `adb devices` attached | ### Environment notes @@ -167,10 +167,32 @@ Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rath - [x] Resolve missing `google-services.json` without committing secrets. - [x] `assembleDebug` succeeds and produces an APK. - [x] Record APK path, size and ABIs. -- [ ] Install APK on at least one ARM64 Android device via ADB. -- [ ] Confirm application reaches setup/first screen without immediate crash. +- [x] Install APK on at least one ARM64 Android device via ADB. +- [x] Confirm application reaches setup/first screen without immediate crash. - [ ] Optional: document a one-command local env setup script (later, if useful). + +## Runtime crash fix (2026-10-06, verified on device) + +Device: nubia NX789J, Android 16 + +Verified root cause via DropBox `data_app_crash`: + +``` +java.lang.IllegalArgumentException: Please set a valid API key. + at com.google.firebase.installations.FirebaseInstallations.preConditionChecks + at com.google.firebase.perf.transport.TransportManager... +``` + +Cause: community `google-services` stub + Firebase Performance. + +Fix verified: + +- Firebase Performance plugin/dependency disabled when community stub is detected +- Play Games SDK / auto sign-in skipped for `BuildConfig.COMMUNITY_BUILD` +- After reinstall: process stays alive on `SetupActivity` (no Theme Hospital data yet) +- No new DropBox crash after the fixed APK install + ## Immediate next task **Do not migrate CorsixTH yet.** @@ -189,3 +211,4 @@ Mandatory documents: - `docs/PROJECT_STATUS.md` Any AI/developer continuing this work must read these files before implementation. + From 9df05fdd0cc07b04a9689273de06487a2aec3cbe Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Tue, 6 Oct 2026 23:52:51 +0200 Subject: [PATCH 10/23] docs: add CorsixTH 0.70.1 Android migration matrix Record the Phase 2 delta between the historical Android fork and upstream v0.70.1, close Phases 1-2 in project status, and link the matrix from the README. Co-authored-by: Cursor --- README.md | 1 + docs/MIGRATION_MATRIX_0.70.1.md | 136 ++++++++++++++++++++ docs/PROJECT_STATUS.md | 218 ++++++++++---------------------- 3 files changed, 205 insertions(+), 150 deletions(-) create mode 100644 docs/MIGRATION_MATRIX_0.70.1.md diff --git a/README.md b/README.md index 76a69e79e..07bc65151 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ 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/AI_AGENT_RULES.md`](docs/AI_AGENT_RULES.md) — rules for AI-assisted development --- 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/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index c0f63f78d..57027e347 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,4 +1,4 @@ -# CorsixTH Android Modernization — Project Status +# CorsixTH Android Modernization — Project Status Last updated: 2026-10-06 @@ -14,140 +14,92 @@ Base branch: ## Current phase -**Phase 1 — Reproducible local build of the inherited port** +**Phase 2 — Android fork vs CorsixTH 0.70.1 delta analysis** -Status: **PARTIAL — assembleDebug verified on Windows; device install not yet validated** +Status: **DONE** (migration matrix written; no core replacement) Previous: -**Phase 0 — Baseline and repository control** — documentation established; local toolchain inventory completed via the Phase 1 build attempt. +- **Phase 0** — repository control / docs — DONE +- **Phase 1** — reproducible local Windows build + device launch to SetupActivity — DONE -## GitHub Actions status +Next phase: -**DO NOT RUN without explicit repository-owner approval.** +**Phase 3 — Toolchain and native build modernization for 0.70.1 acceptance** -The inherited workflows are retained as-is for reference only. +## GitHub Actions status -Development validation is currently local-first. +**DO NOT RUN without explicit repository-owner approval.** -Self-hosted runner `GAMING` exists but must not be used without explicit approval. +Local-first validation only. Self-hosted runner `GAMING` must not be used without approval. ## Modernization target -First target core: - -`CorsixTH 0.70.1` - -First target architecture: - -`arm64-v8a` - -Primary Android targets: - -- Android 16 -- Android 15 -- Android 14 +- Core: **CorsixTH 0.70.1** +- ABI priority: **arm64-v8a** +- Android targets: 14 / 15 / 16 +- Multimedia: **SDL2** (SDL3 deferred) -SDL target for the first modernization cycle: +## Phase 1 — verified Windows + device baseline -`SDL2` +| Item | Verified value | +|---|---| +| Host OS | Windows 11 Professionnel (`10.0.26200`) | +| Branch | `feature/corsixth-0.70-modernization` | +| CorsixTH submodule | `fa1ca3e2` (`alanwoolley/CorsixTH`) | +| JDK | Temurin **17.0.14+7** | +| Gradle / AGP / Kotlin | **8.9** / **8.7.3** / **2.0.21** | +| SDK | `E:\000-lastBeacon_ia\android-sdk` (Platform 35, Build-Tools 35.0.1) | +| NDK | **27.0.12077973** (pinned via `android.ndkVersion`) | +| `assembleDebug` | **SUCCESS** (~8m 43s cold native) | +| APK | `build/outputs/apk/debug/CorsixTH-Android-debug.apk` (~76 MiB) | +| ABIs | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` | +| Device | Nubia NX789J, Android 16 | +| Runtime | SetupActivity reached; community build no longer crashes | + +### Community runtime crash (fixed) + +DropBox root cause: + +```text +IllegalArgumentException: Please set a valid API key + at FirebaseInstallations / Firebase Performance TransportManager +``` -SDL3 is deferred to a future independent phase. +Fix: disable Firebase Performance + Play Games init when community `google-services` stub is detected (`BuildConfig.COMMUNITY_BUILD`). -## Verified Windows baseline (2026-10-06) +## Phase 2 — delta analysis summary -The following values were **verified by executing** a local Windows build, not only by reading source. +Deliverable: [`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md) -| Item | Verified value | +| Item | Result | |---|---| -| Host OS | Windows 11 Professionnel (`10.0.26200`) | -| Branch | `feature/corsixth-0.70-modernization` | -| HEAD at build time | `dba08e90d336f246b399e42c63e7fed9d3fab89f` (+ local Phase-1 build fixes) | -| CorsixTH submodule | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` (`alanwoolley/CorsixTH`, detached at Android fork revision) | -| JDK | Eclipse Temurin / OpenJDK **17.0.14+7** (portable local install) | -| Gradle wrapper | **8.9** | -| Android Gradle Plugin | **8.7.3** | -| Kotlin | **2.0.21** | -| Android SDK root used | `E:\000-lastBeacon_ia\android-sdk` | -| Platform | `platforms;android-35` | -| Build-Tools | `35.0.1` | -| Platform-Tools / ADB | `37.0.1` | -| NDK used | **27.0.12077973** (`r27`) | -| NDK path | `E:\000-lastBeacon_ia\android-sdk\ndk\27.0.12077973` | -| Native entry | `jni/Android.mk` via `ndkBuild` | -| STL / platform | `c++_static` / `android-27` | -| Command | `.\gradlew.bat assembleDebug --stacktrace` | -| Result | **BUILD SUCCESSFUL in 8m 43s** | -| APK path | `build/outputs/apk/debug/CorsixTH-Android-debug.apk` | -| APK size | **76.47 MiB** (80 179 730 bytes) | -| APK package | `uk.co.armedpineapple.cth` | -| APK versionName | `1.0.0-SNAPSHOT` | -| ABIs in APK | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` | -| Native libs observed | `libappmain.so`, `libSDL2.so`, `libSDL2_mixer.so`, `libLUA.so`, `libinnoextract.so`, Crashlytics NDK libs | -| Device test | **Not performed** — no authorized `adb devices` attached | - -### Environment notes - -- At session start, `JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and `java`/`adb` on PATH were unset. -- A partial Android SDK already existed at `E:\000-lastBeacon_ia\android-sdk` (platform 35 + platform-tools). -- Missing components installed with `sdkmanager`: `build-tools;35.0.1`, `ndk;27.0.12077973`. -- Portable JDK extracted under `E:\00-Dev_After_Renamer\101-CrosixTH_Android\tools\jdk-17.0.14+7` (outside the Git repository). -- Local `local.properties` points to the SDK root and is gitignored. - -### Build blockers encountered and fixes - -1. **Missing `google-services.json`** - - First root failure: `:processDebugGoogleServices` - - Minimal reversible fix: committed community stub at `distribution/community/google-services.community.json` and copy-into-place logic in `build.gradle` when the private file is absent. - - Private/real `google-services.json` remains gitignored and is preferred when present. - -2. **NDK selection not pinned in Gradle** - - Successful baseline used NDK **27.0.12077973**. - - `android.ndkVersion "27.0.12077973"` was added so future builds do not depend on deprecated `ndk.dir`. - -### Important warnings observed (non-fatal) - -- Deprecated `ASensorManager_getInstance` in SDL Android sensor code. -- `th_lua.cpp` non-void function missing return warning. -- Firebase Performance ASM instrumentation could not resolve some AndroidX/Window classes. -- Room `annotationProcessor` vs `kapt` configuration warning for `room-compiler`. - -## Inherited repository configuration (still accurate) - -| Component | Current inherited value | +| Upstream compared | `CorsixTH/CorsixTH` **v0.70.1** (`56bd5d00`) | +| Android submodule | `fa1ca3e2` | +| Approx. merge-base | `fc38c4f4` (near **0.67** release bump) | +| Divergence | ~157 Android-only commits vs ~1235 upstream commits | +| Blind submodule replace? | **No** | +| Critical Android glue | `Android/androidhooks.*`, `Android/androidevents.*`, `sdl_core` hook, Lua `TH.android*`, menus/touch, `appmain.cpp` | +| Already C++17? | **Yes** in `jni/CorsixTH/Android.mk` (`-std=c++17`) | +| First deferrals | MIDI/FluidSynth stack; full Play Games/achievements; SDL3 | + +## Inherited configuration (still accurate) + +| Component | Value | |---|---| | Default/base branch | `dev` | -| Android Gradle Plugin | `8.7.3` | -| Kotlin | `2.0.21` | -| KSP | `2.0.21-1.0.28` | -| Gradle wrapper | `8.9` | -| `minSdkVersion` | `27` | -| `targetSdkVersion` | `35` | -| `compileSdk` | `35` | -| Native build entry | `jni/Android.mk` via `ndkBuild` | -| STL | `c++_static` | -| NDK platform declaration | `android-27` | -| CorsixTH submodule URL | `https://github.com/alanwoolley/CorsixTH.git` | -| CorsixTH submodule branch hint | `Android` | -| CorsixTH submodule gitlink | `fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0` | -| Firebase/Google plugins | Present in inherited build; community stub allows local compile | -| Google Play Games | Present in inherited build | - -## Known architectural concern - -The Android app does not directly track upstream CorsixTH. It references the dedicated historical Android fork/branch: - -`alanwoolley/CorsixTH : Android` - -Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rather than a direct submodule replacement. +| `minSdk` / `targetSdk` / `compileSdk` | 27 / 35 / 35 | +| Native entry | `jni/Android.mk` via `ndkBuild` | +| STL / NDK platform | `c++_static` / `android-27` | +| CorsixTH submodule URL | `https://github.com/alanwoolley/CorsixTH.git` (branch hint `Android`) | ## Phase tracking | Phase | Description | State | |---|---|---| -| 0 | Baseline/repository control | DONE (docs + toolchain inventory via Phase 1) | -| 1 | Reproducible inherited local build | PARTIAL (`assembleDebug` OK; device install pending) | -| 2 | Android fork vs 0.70.1 delta analysis | NOT STARTED | +| 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 | NOT STARTED | | 4 | CorsixTH 0.70.1 bootstrap | NOT STARTED | | 5 | Data packaging/import | NOT STARTED | @@ -158,48 +110,16 @@ Therefore the migration to CorsixTH 0.70.1 must begin with a delta analysis rath | 10 | Release engineering | NOT STARTED | | 11 | SDL3 migration | FUTURE / OUT OF SCOPE | -## Phase 1 remaining checklist - -- [x] Initialize historical CorsixTH submodule at recorded revision. -- [x] Install/pin JDK 17 for AGP 8.7.3. -- [x] Use repository Gradle wrapper 8.9. -- [x] Install Android SDK Platform 35, Build-Tools 35.0.1, NDK 27.0.12077973. -- [x] Resolve missing `google-services.json` without committing secrets. -- [x] `assembleDebug` succeeds and produces an APK. -- [x] Record APK path, size and ABIs. -- [x] Install APK on at least one ARM64 Android device via ADB. -- [x] Confirm application reaches setup/first screen without immediate crash. -- [ ] Optional: document a one-command local env setup script (later, if useful). - - -## Runtime crash fix (2026-10-06, verified on device) - -Device: nubia NX789J, Android 16 - -Verified root cause via DropBox `data_app_crash`: - -``` -java.lang.IllegalArgumentException: Please set a valid API key. - at com.google.firebase.installations.FirebaseInstallations.preConditionChecks - at com.google.firebase.perf.transport.TransportManager... -``` - -Cause: community `google-services` stub + Firebase Performance. - -Fix verified: - -- Firebase Performance plugin/dependency disabled when community stub is detected -- Play Games SDK / auto sign-in skipped for `BuildConfig.COMMUNITY_BUILD` -- After reinstall: process stays alive on `SetupActivity` (no Theme Hospital data yet) -- No new DropBox crash after the fixed APK install - ## Immediate next task -**Do not migrate CorsixTH yet.** +Start **Phase 3** only: -Next engineering task: install the debug APK on an ARM64 Android device/emulator and confirm the first-launch/setup screen, then close Phase 1. +1. Extend/adapt `Android.mk` + Android `config.h` so 0.70.1 sources can be accepted. +2. Decide PNG strategy (upstream libpng vs current lodepng). +3. Keep SDL2; do not enable MIDI yet. +4. Do **not** replace the submodule until a compile plan from the matrix is ready. -No workflow execution is required for Phase 1. +No GitHub Actions. No push unless owner requests it. ## Documentation control @@ -209,6 +129,4 @@ Mandatory documents: - `docs/DEVELOPMENT_PHASES.md` - `docs/AI_AGENT_RULES.md` - `docs/PROJECT_STATUS.md` - -Any AI/developer continuing this work must read these files before implementation. - +- `docs/MIGRATION_MATRIX_0.70.1.md` (Phase 2 deliverable) From 6fdd483c6bd652c67044b6fc150f967c1b23cc5e Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 00:09:36 +0200 Subject: [PATCH 11/23] build: enforce ARM64 C++17 ndk-build toolchain for Phase 3 Pin APP_ABI/abiFilters to arm64-v8a, document native dependency readiness, and advance the CorsixTH submodule gitlink to the local Phase-3 toolchain commit. Historical engine sources are unchanged. Co-authored-by: Cursor --- README.md | 1 + build.gradle | 6 ++ docs/PHASE3_NATIVE_TOOLCHAIN.md | 113 ++++++++++++++++++++++++++++++++ docs/PROJECT_STATUS.md | 93 ++++++++------------------ jni/Application.mk | 12 ++++ jni/CorsixTH | 2 +- 6 files changed, 161 insertions(+), 66 deletions(-) create mode 100644 docs/PHASE3_NATIVE_TOOLCHAIN.md diff --git a/README.md b/README.md index 07bc65151..8a26de0c7 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ Start here if you want to understand or work on the modernization: - [`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 --- diff --git a/build.gradle b/build.gradle index 36be0b896..45ada3957 100644 --- a/build.gradle +++ b/build.gradle @@ -118,6 +118,12 @@ android { versionCode = project.ext.offsetBuildVersionCode buildConfigField "boolean", "COMMUNITY_BUILD", (usingCommunityGoogleServices ? "true" : "false") + + // 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" + } } compileOptions { diff --git a/docs/PHASE3_NATIVE_TOOLCHAIN.md b/docs/PHASE3_NATIVE_TOOLCHAIN.md new file mode 100644 index 000000000..8441153ae --- /dev/null +++ b/docs/PHASE3_NATIVE_TOOLCHAIN.md @@ -0,0 +1,113 @@ +# 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. diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 57027e347..7bd2d7365 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # CorsixTH Android Modernization — Project Status -Last updated: 2026-10-06 +Last updated: 2026-10-07 ## Current branch @@ -14,18 +14,19 @@ Base branch: ## Current phase -**Phase 2 — Android fork vs CorsixTH 0.70.1 delta analysis** +**Phase 3 — Toolchain and native build modernization** -Status: **DONE** (migration matrix written; no core replacement) +Status: **DONE** (C++17 + ARM64 toolchain validated on historical core; submodule not replaced) Previous: -- **Phase 0** — repository control / docs — DONE -- **Phase 1** — reproducible local Windows build + device launch to SetupActivity — DONE +- Phase 0 — DONE +- Phase 1 — DONE (`assembleDebug` + SetupActivity) +- Phase 2 — DONE ([`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md)) Next phase: -**Phase 3 — Toolchain and native build modernization for 0.70.1 acceptance** +**Phase 4 — CorsixTH 0.70.1 core bootstrap** ## GitHub Actions status @@ -36,62 +37,25 @@ Local-first validation only. Self-hosted runner `GAMING` must not be used withou ## Modernization target - Core: **CorsixTH 0.70.1** -- ABI priority: **arm64-v8a** +- ABI priority: **arm64-v8a** (now enforced in build) - Android targets: 14 / 15 / 16 - Multimedia: **SDL2** (SDL3 deferred) -## Phase 1 — verified Windows + device baseline +## Phase 3 summary -| Item | Verified value | -|---|---| -| Host OS | Windows 11 Professionnel (`10.0.26200`) | -| Branch | `feature/corsixth-0.70-modernization` | -| CorsixTH submodule | `fa1ca3e2` (`alanwoolley/CorsixTH`) | -| JDK | Temurin **17.0.14+7** | -| Gradle / AGP / Kotlin | **8.9** / **8.7.3** / **2.0.21** | -| SDK | `E:\000-lastBeacon_ia\android-sdk` (Platform 35, Build-Tools 35.0.1) | -| NDK | **27.0.12077973** (pinned via `android.ndkVersion`) | -| `assembleDebug` | **SUCCESS** (~8m 43s cold native) | -| APK | `build/outputs/apk/debug/CorsixTH-Android-debug.apk` (~76 MiB) | -| ABIs | `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64` | -| Device | Nubia NX789J, Android 16 | -| Runtime | SetupActivity reached; community build no longer crashes | - -### Community runtime crash (fixed) - -DropBox root cause: - -```text -IllegalArgumentException: Please set a valid API key - at FirebaseInstallations / Firebase Performance TransportManager -``` - -Fix: disable Firebase Performance + Play Games init when community `google-services` stub is detected (`BuildConfig.COMMUNITY_BUILD`). - -## Phase 2 — delta analysis summary - -Deliverable: [`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md) +Deliverable: [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md) | Item | Result | |---|---| -| Upstream compared | `CorsixTH/CorsixTH` **v0.70.1** (`56bd5d00`) | -| Android submodule | `fa1ca3e2` | -| Approx. merge-base | `fc38c4f4` (near **0.67** release bump) | -| Divergence | ~157 Android-only commits vs ~1235 upstream commits | -| Blind submodule replace? | **No** | -| Critical Android glue | `Android/androidhooks.*`, `Android/androidevents.*`, `sdl_core` hook, Lua `TH.android*`, menus/touch, `appmain.cpp` | -| Already C++17? | **Yes** in `jni/CorsixTH/Android.mk` (`-std=c++17`) | -| First deferrals | MIDI/FluidSynth stack; full Play Games/achievements; SDL3 | - -## Inherited configuration (still accurate) - -| Component | Value | -|---|---| -| Default/base branch | `dev` | -| `minSdk` / `targetSdk` / `compileSdk` | 27 / 35 / 35 | -| Native entry | `jni/Android.mk` via `ndkBuild` | -| STL / NDK platform | `c++_static` / `android-27` | -| CorsixTH submodule URL | `https://github.com/alanwoolley/CorsixTH.git` (branch hint `Android`) | +| C++17 | `APP_CPPFLAGS` + engine `LOCAL_CPPFLAGS` | +| STL / platform | `c++_static` / `android-27` | +| ABI | **`arm64-v8a` only** (`Application.mk` + `ndk.abiFilters`) | +| NDK | 27.0.12077973 | +| SDL2 | retained (2.30.10) / mixer 2.6.3 | +| PNG strategy | keep lodepng for first 0.70.1 attempt; prefer libpng later | +| MIDI | deferred | +| Submodule | still `fa1ca3e2` (historical Android fork) | +| Validation build | **SUCCESS**; APK ~43.5 MiB arm64-only | ## Phase tracking @@ -100,7 +64,7 @@ Deliverable: [`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md) | 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 | NOT STARTED | +| 3 | C++17/native toolchain modernization | DONE | | 4 | CorsixTH 0.70.1 bootstrap | NOT STARTED | | 5 | Data packaging/import | NOT STARTED | | 6 | Input/UI/audio/gameplay essentials | NOT STARTED | @@ -112,21 +76,20 @@ Deliverable: [`docs/MIGRATION_MATRIX_0.70.1.md`](MIGRATION_MATRIX_0.70.1.md) ## Immediate next task -Start **Phase 3** only: +Start **Phase 4** carefully: -1. Extend/adapt `Android.mk` + Android `config.h` so 0.70.1 sources can be accepted. -2. Decide PNG strategy (upstream libpng vs current lodepng). -3. Keep SDL2; do not enable MIDI yet. -4. Do **not** replace the submodule until a compile plan from the matrix is ready. +1. Introduce CorsixTH 0.70.1 sources without discarding Android glue. +2. Update `Android.mk` source list (`th_strings.cpp`, …). +3. Re-apply `Android/` hooks onto 0.70.1 `sdl_core` / `th_lua`. +4. Aim for compile → link → load `libappmain.so` → visible bootstrap. -No GitHub Actions. No push unless owner requests it. +Do **not** enable MIDI or SDL3 in that first bootstrap. ## Documentation control -Mandatory documents: - - `DEVELOPMENT_POLICY.md` - `docs/DEVELOPMENT_PHASES.md` - `docs/AI_AGENT_RULES.md` - `docs/PROJECT_STATUS.md` -- `docs/MIGRATION_MATRIX_0.70.1.md` (Phase 2 deliverable) +- `docs/MIGRATION_MATRIX_0.70.1.md` +- `docs/PHASE3_NATIVE_TOOLCHAIN.md` diff --git a/jni/Application.mk b/jni/Application.mk index 552c60f1d..37a26f9a9 100644 --- a/jni/Application.mk +++ b/jni/Application.mk @@ -1,2 +1,14 @@ +# 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 diff --git a/jni/CorsixTH b/jni/CorsixTH index fa1ca3e2d..473312fad 160000 --- a/jni/CorsixTH +++ b/jni/CorsixTH @@ -1 +1 @@ -Subproject commit fa1ca3e2d9bbe3721351040bc98812bdf1f0bac0 +Subproject commit 473312fadf6bae7df716168afe3983cb1453b96a From acb5f0b74f49919feef12efd887e400cfaf64110 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 00:09:58 +0200 Subject: [PATCH 12/23] docs: note local-only CorsixTH submodule tip for Phase 3 Co-authored-by: Cursor --- docs/PHASE3_NATIVE_TOOLCHAIN.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/PHASE3_NATIVE_TOOLCHAIN.md b/docs/PHASE3_NATIVE_TOOLCHAIN.md index 8441153ae..62d77e8ca 100644 --- a/docs/PHASE3_NATIVE_TOOLCHAIN.md +++ b/docs/PHASE3_NATIVE_TOOLCHAIN.md @@ -111,3 +111,11 @@ $env:ANDROID_SDK_ROOT = "E:\000-lastBeacon_ia\android-sdk" | 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. From a6060856d3c32128e887a156cc9710d68d80da49 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:04:35 +0200 Subject: [PATCH 13/23] chore(release): backup Phases 4-7 + soft keyboard as 0.70.1-pre-release Point CorsixTH submodule at thomasbonsirven/CorsixTH-android-0.70.1, add self-hosted GAMING workflows, bilingual pre-release notes, and the GameActivity IME bridge for player-name text entry. Co-authored-by: Cursor --- .github/workflows/android-ci.yml | 28 +- .github/workflows/build_and_sign.yml | 57 ++-- .github/workflows/pre-release.yml | 91 ++++++ .gitmodules | 4 +- AndroidManifest.xml | 8 +- build.gradle | 16 +- .../pre-release/0.70.1-pre-release.md | 91 ++++++ docs/PHASE4_BOOTSTRAP_0.70.1.md | 83 ++++++ docs/PHASE5_DATA_PACKAGING.md | 77 +++++ docs/PHASE6_GAMEPLAY_ESSENTIALS.md | 75 +++++ docs/PHASE7_SAVE_LOAD_LIFECYCLE.md | 34 +++ docs/PROJECT_STATUS.md | 81 +++++- jni/CorsixTH | 2 +- .../main/java/org/libsdl/app/SDLActivity.java | 36 ++- res/raw/config_template.txt | 2 +- src/Java/org/libsdl/app/CthImeBridge.java | 36 +++ .../uk/co/armedpineapple/cth/GameActivity.kt | 273 +++++++++++++++++- .../armedpineapple/cth/files/FilesService.kt | 184 ++++++++++-- .../armedpineapple/cth/setup/SetupFragment.kt | 7 + .../cth/setup/SetupViewModel.kt | 43 ++- versioning.gradle | 4 +- 21 files changed, 1110 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/pre-release.yml create mode 100644 distribution/pre-release/0.70.1-pre-release.md create mode 100644 docs/PHASE4_BOOTSTRAP_0.70.1.md create mode 100644 docs/PHASE5_DATA_PACKAGING.md create mode 100644 docs/PHASE6_GAMEPLAY_ESSENTIALS.md create mode 100644 docs/PHASE7_SAVE_LOAD_LIFECYCLE.md create mode 100644 src/Java/org/libsdl/app/CthImeBridge.java diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 9125716af..c7c6b6821 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@v4 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: pwsh + run: .\gradlew.bat createGameZip assembleDebug --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..01f3b384d 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@v4 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: pwsh + run: | + "IS_SNAPSHOT=true" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build APK and app bundle - run: ./gradlew bundleRelease assembleRelease + shell: pwsh + run: .\gradlew.bat bundleRelease assembleRelease --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: pwsh + run: .\gradlew.bat uploadCrashlyticsSymbolFileRelease --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/* - name: Upload Artifacts - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: name: APK path: build/outputs/apk/release/* - name: Set version variables + shell: pwsh 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..2d773b389 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,91 @@ +# Pre-release APK on the self-hosted GAMING runner (Windows / X64). +name: Pre-release APK + +on: + push: + tags: + - 'v*-pre-release' + workflow_dispatch: + inputs: + build_version: + description: 'BUILD_VERSION / release name (e.g. 0.70.1-pre-release)' + required: true + default: '0.70.1-pre-release' + +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@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Resolve version + shell: pwsh + run: | + $ver = "${{ github.event.inputs.build_version }}" + if ([string]::IsNullOrWhiteSpace($ver)) { + $tag = "${{ github.ref_name }}" + if ($tag.StartsWith("v")) { $ver = $tag.Substring(1) } else { $ver = $tag } + } + "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 + Write-Host "BUILD_VERSION=$ver" + + - name: Build release APK + shell: pwsh + run: | + # Community/local signing: omit RELEASE_STORE_* to use debug keystore. + # Set KEYSTORE + RELEASE_* secrets on the repo for a real release signature. + .\gradlew.bat createGameZip assembleRelease --no-daemon + env: + BUILD_VERSION: ${{ env.BUILD_VERSION }} + IS_SNAPSHOT: 'false' + BUILD_NUMBER: ${{ github.run_number }} + RELEASE_STORE_FILE: ${{ secrets.RELEASE_STORE_FILE }} + RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} + RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} + + - name: Stage APK artifact + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path distribution/pre-release | Out-Null + $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 + Copy-Item $src.FullName "build/outputs/apk/release/CorsixTH-Android-$($env:BUILD_VERSION).apk" -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: Create 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 + 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..44efa3c56 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -16,7 +16,6 @@ android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.AppTheme" - android:gwpAsanMode="always" tools:targetApi="r"> + + + diff --git a/build.gradle b/build.gradle index 45ada3957..6d1b2cdf5 100644 --- a/build.gradle +++ b/build.gradle @@ -172,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) @@ -205,7 +212,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') @@ -217,8 +224,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' } } 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..fa71c4ed1 --- /dev/null +++ b/distribution/pre-release/0.70.1-pre-release.md @@ -0,0 +1,91 @@ +# CorsixTH-Android 0.70.1-pre-release + +**Branch:** `feature/corsixth-0.70-modernization` +**Tag:** `v0.70.1-pre-release` +**Status:** Pre-release (Phases 0–7 + soft keyboard). Phase 8+ not started. + +--- + +## English + +### Highlights +- CorsixTH **0.70.1** engine on Android (ARM64 / `arm64-v8a` only). +- Modern toolchain: AGP 8.x, Gradle 8.9, Kotlin 2.0, NDK 27, C++17. +- Theme Hospital data import via SAF (GOG / retail), engine packaging via `game.zip`. +- Soft keyboard for player name and other textboxes (IME bridge). + +### What was done (Phases 0–7) +| Area | Details | +|---|---| +| Build / baseline | Reproducible local `assembleDebug`; community build without Play Services secrets | +| Migration | Fork vs upstream 0.70.1 matrix; ARM64-only native build | +| Bootstrap | Android glue restored (hooks, events, touch, settings); C++ `mainloop` after init | +| Data | `game.zip` + engine upgrade markers; SAF import with nested-root resolve | +| Gameplay UI | Touch synthesis, back→Escape, save/load hooks, Play Games UI removed from campaign flow | +| Lifecycle | Launch after GOG import fixed; music dir / intro / palette shims for Android | + +### Keyboard fix (this checkpoint) +- **Problem:** Soft keyboard never appeared on “Player name”; when it did, pre-filled `PLAYER` could not be deleted. +- **Cause:** `window.lua` used global `SDL` / `TH` (nil — modules are `require` locals), so IME calls never ran. Immersive `SurfaceView` also stole focus from SDL `DummyEdit`. +- **Fix:** + - `require("TH")` / `require("sdl")` in `window.lua`; request IME on textbox activate/tick + - `GameActivity` EditText IME bridge + `CthImeBridge` → SDL text/backspace + - Backspace on empty IME buffer forwards deletes into the in-game textbox + - SDL `showTextInput` prefers GameActivity soft-keyboard path + +### Not in this pre-release +- Phase 8 Android 14/15/16 hardening +- Phase 9 community/Google decoupling +- Phase 10 release engineering / Play Store +- SDL3 / MIDI (deferred) + +### Install notes +- APK is a **pre-release**, signed with the debug keystore unless CI secrets are present. +- Theme Hospital original data still required (setup wizard / SAF import). +- Prefer a clean install or accept debug `ALWAYS_UPGRADE` behaviour from prior 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. +- Import des données Theme Hospital via SAF (GOG / boîte), moteur via `game.zip`. +- Clavier logiciel pour le nom du joueur et les autres champs texte (pont IME). + +### Réalisé (phases 0–7) +| Domaine | Détails | +|---|---| +| Build / baseline | `assembleDebug` local reproductible ; build community sans secrets Play Services | +| Migration | Matrice fork vs upstream 0.70.1 ; natif ARM64 seul | +| Bootstrap | Colle Android (hooks, events, touch, réglages) ; `mainloop` C++ après init | +| Données | `game.zip` + marqueurs d’upgrade ; import SAF avec résolution de racine imbriquée | +| UI gameplay | Touch, Retour→Échap, save/load, boutons Play Games retirés du flux campagne | +| Cycle de vie | Lancement après import GOG corrigé ; shims musique / intro / palette Android | + +### Correctif clavier (ce jalon) +- **Problème :** le clavier n’apparaissait pas sur « Nom du joueur » ; une fois affiché, impossible d’effacer le `PLAYER` prérempli. +- **Cause :** `window.lua` appelait les globaux `SDL` / `TH` (inexistants — modules en `require` local), donc l’IME n’était jamais demandé. Le `SurfaceView` immersif volait aussi le focus à `DummyEdit` SDL. +- **Correction :** + - `require("TH")` / `require("sdl")` dans `window.lua` ; demande IME à l’activation / tick du textbox + - Pont `EditText` dans `GameActivity` + `CthImeBridge` → texte / retour arrière SDL + - Retour arrière sur buffer IME vide transmis au textbox du jeu + - `showTextInput` SDL privilégie le chemin clavier de `GameActivity` + +### Hors périmètre de cette pre-release +- Phase 8 durcissement Android 14/15/16 +- Phase 9 découplage community / Google +- Phase 10 engineering release / Play Store +- SDL3 / MIDI (reportés) + +### Installation +- APK **pre-release**, signé debug sauf si les secrets CI de signature sont fournis. +- Données Theme Hospital toujours requises (assistant / import SAF). +- Installation propre recommandée si une ancienne build debug a laissé un état moteur. + +--- + +## Artifact + +- `CorsixTH-Android-0.70.1-pre-release.apk` (arm64-v8a) 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/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 7bd2d7365..6493ddcbc 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -14,19 +14,25 @@ Base branch: ## Current phase -**Phase 3 — Toolchain and native build modernization** +**Phase 7 — Save/load and lifecycle reliability** -Status: **DONE** (C++17 + ARM64 toolchain validated on historical core; submodule not replaced) +Status: **DONE** (device smoke + soft keyboard accepted) + +Checkpoint: **`v0.70.1-pre-release`** (APK + bilingual notes) Previous: - Phase 0 — DONE - Phase 1 — DONE (`assembleDebug` + SetupActivity) - 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)) Next phase: -**Phase 4 — CorsixTH 0.70.1 core bootstrap** +**Phase 8 — Android 14/15/16 hardening** ## GitHub Actions status @@ -54,8 +60,45 @@ Deliverable: [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md) | SDL2 | retained (2.30.10) / mixer 2.6.3 | | PNG strategy | keep lodepng for first 0.70.1 attempt; prefer libpng later | | MIDI | deferred | -| Submodule | still `fa1ca3e2` (historical Android fork) | -| Validation build | **SUCCESS**; APK ~43.5 MiB arm64-only | +| Submodule | Phase 4 branch `feature/android-phase4-0.70.1` (0.70.1 + Android glue) | +| Validation build | **SUCCESS**; APK ~43.9 MiB arm64-only | + +## Phase 4 summary + +Deliverable: [`docs/PHASE4_BOOTSTRAP_0.70.1.md`](PHASE4_BOOTSTRAP_0.70.1.md) + +| Item | Result | +|---|---| +| Core | CorsixTH **0.70.1** sources imported | +| Android glue | hooks/events + Lua touch/settings restored | +| Native | `libappmain` builds (lodepng PNG; MIDI device off) | +| Device | GameActivity **Resumed** after `fixConfig` nil-env fix | +| Push | not done (local commits only when requested) | + +## Phase 5 summary + +Deliverable: [`docs/PHASE5_DATA_PACKAGING.md`](PHASE5_DATA_PACKAGING.md) + +| Item | Result | +|---|---| +| `game.zip` | 0.70.1 layout; Bitmap build helpers excluded | +| Engine upgrade | markers + `api_version` **2717** + `.cth_engine_stamp` | +| TH import | SAF persistable; nested-root resolve; post-import validate | +| Setup wizard | confirmed when TH missing | +| Game with TH | GameActivity resumes; data found after restart | + +## Phase 6 summary + +Deliverable: [`docs/PHASE6_GAMEPLAY_ESSENTIALS.md`](PHASE6_GAMEPLAY_ESSENTIALS.md) + +| Item | Result | +|---|---| +| Touch / scroll | Phase 4 synthesis retained | +| Capture mouse | forced off for Android | +| Back button | trapped → Escape/cancel | +| Save hooks | menu-bar swap + save DB update | +| Play / stats | Android events + Play menu restored | +| Device smoke | pending (ADB offline at close) | ## Phase tracking @@ -65,10 +108,10 @@ Deliverable: [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md) | 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 | NOT STARTED | -| 5 | Data packaging/import | NOT STARTED | -| 6 | Input/UI/audio/gameplay essentials | NOT STARTED | -| 7 | Save/load/lifecycle reliability | NOT STARTED | +| 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 | NOT STARTED | | 9 | Community build cleanup | NOT STARTED | | 10 | Release engineering | NOT STARTED | @@ -76,14 +119,17 @@ Deliverable: [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md) ## Immediate next task -Start **Phase 4** carefully: +**Phase 8** — Android 14/15/16 hardening (scoped storage, exported components, etc.). + +Do **not** enable MIDI or SDL3 yet. + +CI: GitHub Actions must use self-hosted runner **GAMING** (`runs-on: [self-hosted, Windows, X64]`). -1. Introduce CorsixTH 0.70.1 sources without discarding Android glue. -2. Update `Android.mk` source list (`th_strings.cpp`, …). -3. Re-apply `Android/` hooks onto 0.70.1 `sdl_core` / `th_lua`. -4. Aim for compile → link → load `libappmain.so` → visible bootstrap. +## Soft keyboard (post Phase 7) -Do **not** enable MIDI or SDL3 in that first bootstrap. +- 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 @@ -93,3 +139,8 @@ Do **not** enable MIDI or SDL3 in that first bootstrap. - `docs/PROJECT_STATUS.md` - `docs/MIGRATION_MATRIX_0.70.1.md` - `docs/PHASE3_NATIVE_TOOLCHAIN.md` +- `docs/PHASE4_BOOTSTRAP_0.70.1.md` +- `docs/PHASE5_DATA_PACKAGING.md` +- `docs/PHASE6_GAMEPLAY_ESSENTIALS.md` +- `docs/PHASE7_SAVE_LOAD_LIFECYCLE.md` +- `distribution/pre-release/0.70.1-pre-release.md` diff --git a/jni/CorsixTH b/jni/CorsixTH index 473312fad..81b2f84ca 160000 --- a/jni/CorsixTH +++ b/jni/CorsixTH @@ -1 +1 @@ -Subproject commit 473312fadf6bae7df716168afe3983cb1453b96a +Subproject commit 81b2f84ca1eb6cda627aef7928d82eb7e91174d4 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/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/GameActivity.kt b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt index a57c90e5e..80b03a43c 100644 --- a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt +++ b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt @@ -3,8 +3,23 @@ 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 @@ -15,6 +30,7 @@ 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 @@ -50,6 +66,11 @@ class GameActivity : SDLActivity(), Loggable { private var playGamesService: PlayGamesService? = 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 { GameEventHandler(statisticsService) @@ -83,12 +104,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 @@ -224,14 +246,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); @@ -249,6 +474,42 @@ 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() { diff --git a/src/Java/uk/co/armedpineapple/cth/files/FilesService.kt b/src/Java/uk/co/armedpineapple/cth/files/FilesService.kt index 8db289986..728d43a01 100644 --- a/src/Java/uk/co/armedpineapple/cth/files/FilesService.kt +++ b/src/Java/uk/co/armedpineapple/cth/files/FilesService.kt @@ -19,9 +19,11 @@ import java.io.OutputStream import java.util.zip.ZipFile /** - * Files service + * Files service for CorsixTH engine assets and Theme Hospital proprietary data. * - * @constructor Create empty Files service + * Engine data (`game.zip`) and Theme Hospital data are kept separate: + * - engine → [GameConfiguration.cthFiles] + * - Theme Hospital → [GameConfiguration.thFiles] */ class FilesService(val ctx: Context) : Loggable { @@ -51,19 +53,33 @@ class FilesService(val ctx: Context) : Loggable { } /** - * Checks whether the game files exist in the location given in the config. + * Checks whether a usable CorsixTH engine payload is installed. * - * @param config The configuration - * @return whether the game files are installed + * Marker files cover both 0.70.1 essentials and Android-specific Lua/UI assets. */ fun hasGameFiles(config: GameConfiguration): Boolean { - // There's little point searching for all the files here. If one is missing, then they - // probably are all missing. We could do something like checking the integrity with a - // checksum, but it doesn't seem worth the extra overhead. - // - // The game script is the important one to get us up and running anyway. We can detect - // in-game whether any of the additional files are missing or corrupt. - return config.cthLaunchScript.exists() + return ENGINE_MARKER_FILES.all { relative -> + File(config.cthFiles, relative).isFile + } + } + + /** + * Whether the installed engine payload should be replaced from `game.zip`. + * + * Uses marker presence plus [EXPECTED_ENGINE_API_VERSION] from `Lua/api_version.lua`. + */ + fun needsEngineDataUpgrade(config: GameConfiguration): Boolean { + if (!hasGameFiles(config)) { + return true + } + val installedApi = readInstalledApiVersion(config) ?: return true + if (installedApi != EXPECTED_ENGINE_API_VERSION) { + warn { + "Engine API $installedApi != expected $EXPECTED_ENGINE_API_VERSION; upgrade required" + } + return true + } + return false } /** @@ -72,7 +88,7 @@ class FilesService(val ctx: Context) : Loggable { * @param config The configuration * @return Whether the demo game files are installed. */ - fun isDemoVersion(config: GameConfiguration) : Boolean { + fun isDemoVersion(config: GameConfiguration): Boolean { return File(config.thFiles, "DATAM/DEMO.DAT").exists() } @@ -85,21 +101,38 @@ class FilesService(val ctx: Context) : Loggable { * @return whether the original TH files are installed */ fun hasOriginalFiles(config: GameConfiguration): Boolean { - // We should be a bit more thorough with checking for the original files, as we have less - // opportunity to detect if there's an issue here. - val expectedFiles = arrayOf( - "QDATA/AREA01V.DAT", - "DATA/LANG-0.DAT", - ) + return hasThemeHospitalLayout(config.thFiles) + } - for (expectedFile in expectedFiles) { - if (!File(config.thFiles, expectedFile).exists()) { - warn { "Wanted TH file $expectedFile but was not found" } - return false + /** + * True when [root] looks like a Theme Hospital install (full or demo). + */ + fun hasThemeHospitalLayout(root: File): Boolean { + return TH_MARKER_FILES.all { relative -> File(root, relative).isFile } + } + + /** + * True when a SAF document tree looks like Theme Hospital data. + */ + fun hasThemeHospitalLayout(root: DocumentFileCompat): Boolean { + return TH_MARKER_FILES.all { relative -> documentHasRelativeFile(root, relative) } + } + + /** + * Resolves the Theme Hospital root inside a SAF tree. + * + * Accepts the selected folder itself, or a single nesting level used by some GOG/folder layouts. + */ + fun resolveThemeHospitalDocumentRoot(root: DocumentFileCompat): DocumentFileCompat? { + if (hasThemeHospitalLayout(root)) { + return root + } + root.listFiles().forEach { child -> + if (child.isDirectory() && hasThemeHospitalLayout(child)) { + return child } } - - return true + return null } /** @@ -110,7 +143,7 @@ class FilesService(val ctx: Context) : Loggable { * @param config The configuration * @return whether the music library is installed */ - fun hasMusicLibrary(config: GameConfiguration) : Boolean { + fun hasMusicLibrary(config: GameConfiguration): Boolean { return File(config.musicLib, "timidity.cfg").exists() } @@ -118,7 +151,6 @@ class FilesService(val ctx: Context) : Loggable { * Installs the CorsixTH game files. * * @param config The configuration that determines the installation location. - * @param ctx The context. * @param progress An optional channel to send progress updates to. */ suspend fun installGameFiles( @@ -130,6 +162,7 @@ class FilesService(val ctx: Context) : Loggable { try { val target = config.cthFiles extractZipFile(assetOut, target, progress) + writeEngineStamp(config) } finally { assetOut.delete() } @@ -139,7 +172,6 @@ class FilesService(val ctx: Context) : Loggable { * Installs the music library. * * @param config The configuration that determines the installation location. - * @param ctx The context. * @param progress An optional channel to send progress updates to. */ suspend fun installMusicLibrary( @@ -169,7 +201,7 @@ class FilesService(val ctx: Context) : Loggable { /** * Installs the original game files. * - * @param source The source document tree + * @param source The source document tree (already resolved to Theme Hospital root) * @param config The configuration that determines the installation location. * @param progress An optional channel to send progress updates to. */ @@ -182,6 +214,42 @@ class FilesService(val ctx: Context) : Loggable { copyDirectoryTree(source, config.thFiles, progress) } + /** + * After a zip extract into [GameConfiguration.thFiles], promote a nested Theme Hospital + * root (common with demo zips) so markers sit directly under `themehospital/`. + * + * @return true if Theme Hospital markers are present after normalization + */ + fun normalizeThemeHospitalInstall(config: GameConfiguration): Boolean { + if (hasOriginalFiles(config)) { + return true + } + + val root = config.thFiles + if (!root.isDirectory) { + return false + } + + val nested = root.listFiles() + ?.firstOrNull { it.isDirectory && hasThemeHospitalLayout(it) } + ?: return false + + val staging = File(root.parentFile, "${root.name}.staging") + if (staging.exists()) { + staging.deleteRecursively() + } + if (!nested.renameTo(staging)) { + return false + } + root.deleteRecursively() + if (!staging.renameTo(root)) { + // Best-effort restore if rename fails mid-flight. + staging.renameTo(nested) + return false + } + return hasOriginalFiles(config) + } + /** * Gets a File corresponding to the given save name. * @@ -197,6 +265,43 @@ class FilesService(val ctx: Context) : Loggable { } } + private fun readInstalledApiVersion(config: GameConfiguration): Int? { + val apiFile = File(config.cthFiles, "Lua/api_version.lua") + if (!apiFile.isFile) { + return null + } + val text = runCatching { apiFile.readText() }.getOrNull() ?: return null + val match = Regex("""return\s+(\d+)\s*;""").find(text) ?: return null + return match.groupValues[1].toIntOrNull() + } + + private fun writeEngineStamp(config: GameConfiguration) { + val stamp = File(config.cthFiles, ENGINE_STAMP_FILE) + stamp.parentFile?.mkdirs() + stamp.writeText( + "api=$EXPECTED_ENGINE_API_VERSION\n" + + "package=0.70.1\n" + ) + } + + private fun documentHasRelativeFile(root: DocumentFileCompat, relativePath: String): Boolean { + var current: DocumentFileCompat = root + val parts = relativePath.split('/') + for ((index, part) in parts.withIndex()) { + val next = current.listFiles().firstOrNull { + it.name.equals(part, ignoreCase = true) + } ?: return false + if (index == parts.lastIndex) { + return !next.isDirectory() + } + if (!next.isDirectory()) { + return false + } + current = next + } + return false + } + private suspend fun copyDirectoryTree( root: DocumentFileCompat, destinationDirectory: File, @@ -368,6 +473,27 @@ class FilesService(val ctx: Context) : Loggable { companion object { private const val ENGINE_ZIP_FILE = "game.zip" private const val MUSIC_ZIP_FILE = "timidity.zip" + private const val ENGINE_STAMP_FILE = ".cth_engine_stamp" + + /** Must match `CorsixTH/Lua/api_version.lua` for the packaged 0.70.1 tree. */ + const val EXPECTED_ENGINE_API_VERSION = 2717 + + private val ENGINE_MARKER_FILES = arrayOf( + "CorsixTH.lua", + "Lua/app.lua", + "Lua/api_version.lua", + "Lua/earthquake.lua", + "Lua/endconditions.lua", + "Lua/dialogs/android_menu_button.lua", + "Lua/dialogs/resizables/android_menu.lua", + "Bitmap/android.dat", + ) + + private val TH_MARKER_FILES = arrayOf( + "QDATA/AREA01V.DAT", + "DATA/LANG-0.DAT", + ) + const val SAVE_GAME_EXTENSION = "sav" const val SAVE_GAME_FILE_SUFFIX = ".$SAVE_GAME_EXTENSION" } diff --git a/src/Java/uk/co/armedpineapple/cth/setup/SetupFragment.kt b/src/Java/uk/co/armedpineapple/cth/setup/SetupFragment.kt index 664b7874d..5196f206c 100644 --- a/src/Java/uk/co/armedpineapple/cth/setup/SetupFragment.kt +++ b/src/Java/uk/co/armedpineapple/cth/setup/SetupFragment.kt @@ -33,6 +33,13 @@ class SetupFragment : Fragment() { documentTreeResultHandler = registerForActivityResult(openDocumentTreeContract) { uri: Uri? -> uri?.let { + // Persist read access so Theme Hospital import survives process death. + try { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + requireContext().contentResolver.takePersistableUriPermission(it, flags) + } catch (e: SecurityException) { + // Temporary grant still allows the immediate copy. + } viewModel.onGameSourceTreeGranted(it) Toast.makeText(requireActivity(), "Copying. Please wait.", Toast.LENGTH_SHORT) .show() diff --git a/src/Java/uk/co/armedpineapple/cth/setup/SetupViewModel.kt b/src/Java/uk/co/armedpineapple/cth/setup/SetupViewModel.kt index 930135fcc..ac79ddf26 100644 --- a/src/Java/uk/co/armedpineapple/cth/setup/SetupViewModel.kt +++ b/src/Java/uk/co/armedpineapple/cth/setup/SetupViewModel.kt @@ -42,22 +42,29 @@ class SetupViewModel(application: Application) : AndroidViewModel(application), mutableIsExtracting.value = true viewModelScope.launch { - withContext(Dispatchers.IO) { + val result = withContext(Dispatchers.IO) { val documentRoot = DocumentFileCompat.fromTreeUri(application, uri) - documentRoot?.let { - val progressChannel = - Channel(Channel.CONFLATED) { progress -> - val adjusted = (progress.progress * 100).toInt() - mutableExtractProgress.postValue(adjusted) - } - filesService.installOriginalFiles( - documentRoot, application.configuration, progressChannel - ) + ?: return@withContext ExtractResult.FAILURE + val thRoot = filesService.resolveThemeHospitalDocumentRoot(documentRoot) + ?: return@withContext ExtractResult.FAILURE + + val progressChannel = + Channel(Channel.CONFLATED) { progress -> + val adjusted = (progress.progress * 100).toInt() + mutableExtractProgress.postValue(adjusted) + } + filesService.installOriginalFiles( + thRoot, application.configuration, progressChannel + ) + if (filesService.hasOriginalFiles(application.configuration)) { + ExtractResult.SUCCESS + } else { + ExtractResult.FAILURE } } mutableIsExtracting.value = false mutableExtractProgress.value = 100 - mutableExtractResult.value = ExtractResult.SUCCESS + mutableExtractResult.value = result } } @@ -107,9 +114,13 @@ class SetupViewModel(application: Application) : AndroidViewModel(application), mutableExtractProgress.postValue(adjusted) } - filesService.nukeOriginalFiles(getApplication().configuration) + val configuration = getApplication().configuration + filesService.nukeOriginalFiles(configuration) filesService.extractZipFile(downloadTmp, thLocation, extractProgress) - mutableExtractResult.postValue(ExtractResult.SUCCESS) + val ok = filesService.normalizeThemeHospitalInstall(configuration) + mutableExtractResult.postValue( + if (ok) ExtractResult.SUCCESS else ExtractResult.FAILURE + ) } catch (e: Exception) { Log.e("CorsixTH", "Failed to download and install", e) mutableExtractResult.postValue(ExtractResult.FAILURE) @@ -135,8 +146,12 @@ class SetupViewModel(application: Application) : AndroidViewModel(application), } override fun onSuccess() { + val ok = filesService.normalizeThemeHospitalInstall(application.configuration) && + filesService.hasOriginalFiles(application.configuration) mutableIsExtracting.postValue(false) - mutableExtractResult.postValue(ExtractResult.SUCCESS) + mutableExtractResult.postValue( + if (ok) ExtractResult.SUCCESS else ExtractResult.FAILURE + ) } } diff --git a/versioning.gradle b/versioning.gradle index a31c94fa1..2bfc9e053 100644 --- a/versioning.gradle +++ b/versioning.gradle @@ -2,5 +2,7 @@ ext { buildVersionCode = Integer.parseInt(System.getenv("BUILD_NUMBER") ?: "999999") offsetBuildVersionCode = 100000000 + buildVersionCode isSnapshot = Boolean.parseBoolean(System.getenv("IS_SNAPSHOT") ?: "false") || (System.getenv("BUILD_NUMBER") == null) - buildVersion = "1.0.0" + (isSnapshot ? "-SNAPSHOT" : ("." + buildVersionCode.toString())) + // BUILD_VERSION overrides the default scheme (e.g. 0.70.1-pre-release). + buildVersion = System.getenv("BUILD_VERSION") + ?: ("1.0.0" + (isSnapshot ? "-SNAPSHOT" : ("." + buildVersionCode.toString()))) } \ No newline at end of file From f72b9d118d12dda4d0201f9b114c566e3d1b1798 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:11:05 +0200 Subject: [PATCH 14/23] ci: use Windows powershell shell on self-hosted GAMING runner The GAMING runner has Windows PowerShell, not pwsh Core. Co-authored-by: Cursor --- .github/workflows/android-ci.yml | 4 ++-- .github/workflows/build_and_sign.yml | 10 +++++----- .github/workflows/pre-release.yml | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index c7c6b6821..4e2566838 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -17,10 +17,10 @@ jobs: with: submodules: 'true' - name: set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Build debug APK (community-safe) - shell: pwsh + shell: powershell run: .\gradlew.bat createGameZip assembleDebug --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 01f3b384d..39e0f3250 100644 --- a/.github/workflows/build_and_sign.yml +++ b/.github/workflows/build_and_sign.yml @@ -16,7 +16,7 @@ jobs: with: submodules: 'true' - name: set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' @@ -35,11 +35,11 @@ jobs: encodedString: ${{ secrets.GOOGLE_SERVICES_JSON }} - name: Set snapshot if: github.ref == 'refs/heads/dev' - shell: pwsh + shell: powershell run: | "IS_SNAPSHOT=true" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build APK and app bundle - shell: pwsh + shell: powershell run: .\gradlew.bat bundleRelease assembleRelease --no-daemon env: RELEASE_STORE_FILE: ${{ steps.decode_keystore.outputs.filePath }} @@ -48,7 +48,7 @@ jobs: RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} BUILD_NUMBER: ${{ github.run_number }} - name: Upload crashlytics symbols - shell: pwsh + shell: powershell run: .\gradlew.bat uploadCrashlyticsSymbolFileRelease --no-daemon - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -61,7 +61,7 @@ jobs: name: APK path: build/outputs/apk/release/* - name: Set version variables - shell: pwsh + shell: powershell run: | $ver = (Get-Content version.txt -Raw).Trim() "VERSION=$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 2d773b389..efbca902d 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -25,13 +25,13 @@ jobs: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Resolve version - shell: pwsh + shell: powershell run: | $ver = "${{ github.event.inputs.build_version }}" if ([string]::IsNullOrWhiteSpace($ver)) { @@ -43,7 +43,7 @@ jobs: Write-Host "BUILD_VERSION=$ver" - name: Build release APK - shell: pwsh + shell: powershell run: | # Community/local signing: omit RELEASE_STORE_* to use debug keystore. # Set KEYSTORE + RELEASE_* secrets on the repo for a real release signature. @@ -58,7 +58,7 @@ jobs: RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} - name: Stage APK artifact - shell: pwsh + shell: powershell run: | New-Item -ItemType Directory -Force -Path distribution/pre-release | Out-Null $src = Get-ChildItem -Recurse build/outputs/apk/release/*.apk | Select-Object -First 1 From 042b82d3dc30d89c796949c708f0d6e11033dd8a Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:12:54 +0200 Subject: [PATCH 15/23] fix(ci): ignore empty RELEASE_STORE_FILE from Actions secrets Empty secret strings were treated as set and broke assembleRelease on GAMING. Co-authored-by: Cursor --- .github/workflows/pre-release.yml | 8 ++------ build.gradle | 9 ++++++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index efbca902d..2635d710a 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -45,17 +45,13 @@ jobs: - name: Build release APK shell: powershell run: | - # Community/local signing: omit RELEASE_STORE_* to use debug keystore. - # Set KEYSTORE + RELEASE_* secrets on the repo for a real release signature. + # Uses debug keystore when RELEASE_STORE_FILE is unset/blank. .\gradlew.bat createGameZip assembleRelease --no-daemon env: BUILD_VERSION: ${{ env.BUILD_VERSION }} IS_SNAPSHOT: 'false' BUILD_NUMBER: ${{ github.run_number }} - RELEASE_STORE_FILE: ${{ secrets.RELEASE_STORE_FILE }} - RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} - RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} - RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} + - name: Stage APK artifact shell: powershell diff --git a/build.gradle b/build.gradle index 6d1b2cdf5..b8911721b 100644 --- a/build.gradle +++ b/build.gradle @@ -135,9 +135,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") @@ -148,7 +150,8 @@ 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 From 017233a69196a431e4c136cf75929dca9128e25a Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:14:21 +0200 Subject: [PATCH 16/23] ci: locate Android SDK on self-hosted GAMING for pre-release builds Co-authored-by: Cursor --- .github/workflows/pre-release.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 2635d710a..def1618db 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -30,6 +30,36 @@ jobs: 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: | From f6372e0f62b3c6ce5a1c519a21e975c77d64f9bd Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:17:36 +0200 Subject: [PATCH 17/23] ci: build pre-release via short C:\cth-pr junction on GAMING Avoid Windows CreateProcess error 87 from oversized ndk-build link lines. Co-authored-by: Cursor --- .github/workflows/pre-release.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index def1618db..25aa445aa 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -72,26 +72,33 @@ jobs: "RELEASE_TAG=v$ver" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Host "BUILD_VERSION=$ver" - - name: Build release APK + - name: Build release APK (short path) shell: powershell run: | - # Uses debug keystore when RELEASE_STORE_FILE is unset/blank. + # Windows ndk-build hits CreateProcess error 87 when the link line is too + # long under GithubActions_Runner\_work\... Mirror into C:\cth-pr. + $short = "C:\cth-pr" + if (Test-Path $short) { cmd /c "rmdir `"$short`"" } + cmd /c "mklink /J `"$short`" `"$env:GITHUB_WORKSPACE`"" + if ($LASTEXITCODE -ne 0) { throw "Failed to create short junction $short" } + "SHORT_WORKSPACE=$short" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Set-Location $short .\gradlew.bat createGameZip assembleRelease --no-daemon env: BUILD_VERSION: ${{ env.BUILD_VERSION }} IS_SNAPSHOT: 'false' BUILD_NUMBER: ${{ github.run_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/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 - Copy-Item $src.FullName "build/outputs/apk/release/CorsixTH-Android-$($env:BUILD_VERSION).apk" -Force Write-Host "Staged $dest ($($src.Length) bytes)" - name: Upload APK artifact From e48576ffdb8189ba16da0ab40e78ef0d8695b4db Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:18:12 +0200 Subject: [PATCH 18/23] ci: copy workspace to C:\cth-pr for short ndk-build paths Co-authored-by: Cursor --- .github/workflows/pre-release.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 25aa445aa..41bb19e2a 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -76,13 +76,18 @@ jobs: shell: powershell run: | # Windows ndk-build hits CreateProcess error 87 when the link line is too - # long under GithubActions_Runner\_work\... Mirror into C:\cth-pr. + # long under GithubActions_Runner\_work\... Copy into a short real path. $short = "C:\cth-pr" - if (Test-Path $short) { cmd /c "rmdir `"$short`"" } - cmd /c "mklink /J `"$short`" `"$env:GITHUB_WORKSPACE`"" - if ($LASTEXITCODE -ne 0) { throw "Failed to create short junction $short" } + 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 assembleRelease --no-daemon env: BUILD_VERSION: ${{ env.BUILD_VERSION }} From 2df0fa8726dc9ddab575838088bba57132a45aa9 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:23:41 +0200 Subject: [PATCH 19/23] ci: grant contents:write so GAMING can update GitHub pre-releases Co-authored-by: Cursor --- .github/workflows/pre-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 41bb19e2a..bd5105799 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -12,6 +12,9 @@ on: required: true default: '0.70.1-pre-release' +permissions: + contents: write + jobs: build-and-release: # Labels on runner GAMING: self-hosted, Windows, X64 @@ -114,16 +117,18 @@ jobs: distribution/pre-release/CorsixTH-Android-${{ env.BUILD_VERSION }}.apk distribution/pre-release/0.70.1-pre-release.md - - name: Create GitHub pre-release + - name: Publish / update GitHub pre-release uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.RELEASE_TAG }} name: ${{ env.BUILD_VERSION }} prerelease: true + make_latest: false 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: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 93da49b43041f994d768e36eaedcf165961ccbb5 Mon Sep 17 00:00:00 2001 From: thomasbonsirven Date: Wed, 7 Oct 2026 05:51:11 +0200 Subject: [PATCH 20/23] Release phases 8-9 community pre-release (not fully tested). Android 16 hardening, community/playstore flavors without Firebase in community, and bilingual pre-release notes for v0.70.1-pre-release-2. Co-authored-by: Cursor --- .github/workflows/android-ci.yml | 2 +- .github/workflows/build_and_sign.yml | 8 +- .github/workflows/pre-release.yml | 7 +- AndroidManifest.xml | 30 ++++- build.gradle | 96 +++++++++------ .../pre-release/0.70.1-pre-release.md | 116 +++++++++--------- docs/PHASE8_ANDROID_HARDENING.md | 69 +++++++++++ docs/PHASE9_COMMUNITY_BUILD.md | 66 ++++++++++ docs/PROJECT_STATUS.md | 94 ++++---------- gradle.properties | 4 +- jni/Application.mk | 4 + .../java/org/libsdl/app/HIDDeviceManager.java | 37 ++++-- res/xml/backup_rules.xml | 11 ++ res/xml/data_extraction_rules.xml | 13 ++ .../armedpineapple/cth/AndroidUiHardening.kt | 28 +++++ .../co/armedpineapple/cth/CTHApplication.kt | 8 +- .../uk/co/armedpineapple/cth/CrashReporter.kt | 24 ++++ .../uk/co/armedpineapple/cth/GameActivity.kt | 11 +- src/Java/uk/co/armedpineapple/cth/Logging.kt | 10 +- .../armedpineapple/cth/PlayGamesController.kt | 10 ++ .../uk/co/armedpineapple/cth/Reporting.kt | 27 ++-- .../armedpineapple/cth/files/SaveActivity.kt | 2 + .../cth/settings/SettingsActivity.kt | 2 + .../armedpineapple/cth/setup/SetupActivity.kt | 2 + .../armedpineapple/cth/setup/SetupFragment.kt | 9 +- src/community/AndroidManifest.xml | 26 ++++ .../cth/DistributionBootstrap.kt | 19 +++ src/community/res/xml/main_preferences.xml | 45 +++++++ .../armedpineapple/cth/AchievementsTracker.kt | 0 .../cth/DistributionBootstrap.kt | 21 ++++ .../cth/FirebaseCrashReporter.kt | 20 +++ .../co/armedpineapple/cth/PlayGamesService.kt | 5 +- 32 files changed, 603 insertions(+), 223 deletions(-) create mode 100644 docs/PHASE8_ANDROID_HARDENING.md create mode 100644 docs/PHASE9_COMMUNITY_BUILD.md create mode 100644 res/xml/backup_rules.xml create mode 100644 res/xml/data_extraction_rules.xml create mode 100644 src/Java/uk/co/armedpineapple/cth/AndroidUiHardening.kt create mode 100644 src/Java/uk/co/armedpineapple/cth/CrashReporter.kt create mode 100644 src/Java/uk/co/armedpineapple/cth/PlayGamesController.kt create mode 100644 src/community/AndroidManifest.xml create mode 100644 src/community/java/uk/co/armedpineapple/cth/DistributionBootstrap.kt create mode 100644 src/community/res/xml/main_preferences.xml rename src/{Java => playstore/java}/uk/co/armedpineapple/cth/AchievementsTracker.kt (100%) create mode 100644 src/playstore/java/uk/co/armedpineapple/cth/DistributionBootstrap.kt create mode 100644 src/playstore/java/uk/co/armedpineapple/cth/FirebaseCrashReporter.kt rename src/{Java => playstore/java}/uk/co/armedpineapple/cth/PlayGamesService.kt (92%) diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 4e2566838..0052d383d 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -23,4 +23,4 @@ jobs: distribution: 'temurin' - name: Build debug APK (community-safe) shell: powershell - run: .\gradlew.bat createGameZip assembleDebug --no-daemon \ No newline at end of file + 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 39e0f3250..f957927b7 100644 --- a/.github/workflows/build_and_sign.yml +++ b/.github/workflows/build_and_sign.yml @@ -40,7 +40,7 @@ jobs: "IS_SNAPSHOT=true" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build APK and app bundle shell: powershell - run: .\gradlew.bat bundleRelease assembleRelease --no-daemon + run: .\gradlew.bat bundlePlaystoreRelease assemblePlaystoreRelease --no-daemon env: RELEASE_STORE_FILE: ${{ steps.decode_keystore.outputs.filePath }} RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} @@ -49,17 +49,17 @@ jobs: BUILD_NUMBER: ${{ github.run_number }} - name: Upload crashlytics symbols shell: powershell - run: .\gradlew.bat uploadCrashlyticsSymbolFileRelease --no-daemon + run: .\gradlew.bat uploadCrashlyticsSymbolFilePlaystoreRelease --no-daemon - name: Upload Artifacts 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@v4 with: name: APK - path: build/outputs/apk/release/* + path: build/outputs/apk/playstore/release/* - name: Set version variables shell: powershell run: | diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index bd5105799..a72d75468 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -91,7 +91,7 @@ jobs: # 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 assembleRelease --no-daemon + .\gradlew.bat createGameZip assembleCommunityRelease --no-daemon env: BUILD_VERSION: ${{ env.BUILD_VERSION }} IS_SNAPSHOT: 'false' @@ -103,7 +103,10 @@ jobs: $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/release/*.apk | Select-Object -First 1 + $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 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 44efa3c56..4d857ae5c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,14 +9,35 @@ android:smallScreens="false" android:xlargeScreens="true" /> + + + + + + + + + + + + + + + + + + tools:targetApi="36"> @@ -64,7 +85,7 @@ android:preferMinimalPostProcessing="true" android:theme="@style/Theme.AppCompat.NoActionBar" android:windowSoftInputMode="adjustResize" - tools:targetApi="r"> + tools:targetApi="36"> @@ -74,18 +95,21 @@ + android:exported="false" + android:enableOnBackInvokedCallback="true" /> diff --git a/build.gradle b/build.gradle index b8911721b..6bcfaa792 100644 --- a/build.gradle +++ b/build.gradle @@ -23,35 +23,27 @@ apply plugin: 'kotlin-android' apply from: 'versioning.gradle' apply plugin: 'kotlin-kapt' -// Local/community baseline: allow assembleDebug without a private CI secret. -// If a real google-services.json is present (gitignored), it is used unchanged. -// Otherwise the committed community stub is copied into place for this build only. -def googleServicesJson = file('google-services.json') -def communityGoogleServicesStub = file('distribution/community/google-services.community.json') +// 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' -if (!googleServicesJson.exists()) { - if (!communityGoogleServicesStub.exists()) { +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( - "google-services.json is missing and no community stub was found at " + - communityGoogleServicesStub.path) + "Play Store build requires google-services.json (gitignored private file).") } - googleServicesJson.text = communityGoogleServicesStub.text - logger.lifecycle("Using community google-services.json stub for local build " + - "(no private Google/Firebase credentials).") -} -def usingCommunityGoogleServices = - googleServicesJson.exists() && - googleServicesJson.text.contains(communityGoogleServicesMarker) - -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' -// Firebase Performance crashes at runtime with an invalid community API key -// (IllegalArgumentException in FirebaseInstallations / TransportManager). -// Keep it only when a real google-services.json is present. -if (!usingCommunityGoogleServices) { + 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 google-services stub detected: Firebase Performance disabled.") + logger.lifecycle("Community configuration: Google Services / Crashlytics / Perf plugins skipped.") } repositories { @@ -84,15 +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' - if (!usingCommunityGoogleServices) { - 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" @@ -112,12 +95,11 @@ android { 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 - buildConfigField "boolean", "COMMUNITY_BUILD", - (usingCommunityGoogleServices ? "true" : "false") // 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). @@ -126,6 +108,19 @@ android { } } + flavorDimensions = ["distribution"] + productFlavors { + community { + dimension "distribution" + isDefault true + buildConfigField "boolean", "COMMUNITY_BUILD", "true" + } + playstore { + dimension "distribution" + buildConfigField "boolean", "COMMUNITY_BUILD", "false" + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -156,8 +151,10 @@ android { } else { signingConfig signingConfigs.debug } - firebaseCrashlytics { - nativeSymbolUploadEnabled true + if (enablePlaystoreGooglePlugins) { + firebaseCrashlytics { + nativeSymbolUploadEnabled true + } } } @@ -205,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 @@ -255,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/pre-release/0.70.1-pre-release.md b/distribution/pre-release/0.70.1-pre-release.md index fa71c4ed1..4dfb285c7 100644 --- a/distribution/pre-release/0.70.1-pre-release.md +++ b/distribution/pre-release/0.70.1-pre-release.md @@ -1,91 +1,87 @@ -# CorsixTH-Android 0.70.1-pre-release +# CorsixTH-Android 0.70.1-pre-release-2 **Branch:** `feature/corsixth-0.70-modernization` -**Tag:** `v0.70.1-pre-release` -**Status:** Pre-release (Phases 0–7 + soft keyboard). Phase 8+ not started. +**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** engine on Android (ARM64 / `arm64-v8a` only). -- Modern toolchain: AGP 8.x, Gradle 8.9, Kotlin 2.0, NDK 27, C++17. -- Theme Hospital data import via SAF (GOG / retail), engine packaging via `game.zip`. -- Soft keyboard for player name and other textboxes (IME bridge). +- 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 -### What was done (Phases 0–7) | Area | Details | |---|---| -| Build / baseline | Reproducible local `assembleDebug`; community build without Play Services secrets | -| Migration | Fork vs upstream 0.70.1 matrix; ARM64-only native build | -| Bootstrap | Android glue restored (hooks, events, touch, settings); C++ `mainloop` after init | -| Data | `game.zip` + engine upgrade markers; SAF import with nested-root resolve | -| Gameplay UI | Touch synthesis, back→Escape, save/load hooks, Play Games UI removed from campaign flow | -| Lifecycle | Launch after GOG import fixed; music dir / intro / palette shims for Android | - -### Keyboard fix (this checkpoint) -- **Problem:** Soft keyboard never appeared on “Player name”; when it did, pre-filled `PLAYER` could not be deleted. -- **Cause:** `window.lua` used global `SDL` / `TH` (nil — modules are `require` locals), so IME calls never ran. Immersive `SurfaceView` also stole focus from SDL `DummyEdit`. -- **Fix:** - - `require("TH")` / `require("sdl")` in `window.lua`; request IME on textbox activate/tick - - `GameActivity` EditText IME bridge + `CthImeBridge` → SDL text/backspace - - Backspace on empty IME buffer forwards deletes into the in-game textbox - - SDL `showTextInput` prefers GameActivity soft-keyboard path +| 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 8 Android 14/15/16 hardening -- Phase 9 community/Google decoupling -- Phase 10 release engineering / Play Store -- SDL3 / MIDI (deferred) +- Phase 10 release engineering / Play Store publishing polish +- SDL3 / MIDI (deferred) +- Full automated device test matrix ### Install notes -- APK is a **pre-release**, signed with the debug keystore unless CI secrets are present. -- Theme Hospital original data still required (setup wizard / SAF import). -- Prefer a clean install or accept debug `ALWAYS_UPGRADE` behaviour from prior debug builds. +- **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. -- Import des données Theme Hospital via SAF (GOG / boîte), moteur via `game.zip`. -- Clavier logiciel pour le nom du joueur et les autres champs texte (pont IME). +- 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é -### Réalisé (phases 0–7) | Domaine | Détails | |---|---| -| Build / baseline | `assembleDebug` local reproductible ; build community sans secrets Play Services | -| Migration | Matrice fork vs upstream 0.70.1 ; natif ARM64 seul | -| Bootstrap | Colle Android (hooks, events, touch, réglages) ; `mainloop` C++ après init | -| Données | `game.zip` + marqueurs d’upgrade ; import SAF avec résolution de racine imbriquée | -| UI gameplay | Touch, Retour→Échap, save/load, boutons Play Games retirés du flux campagne | -| Cycle de vie | Lancement après import GOG corrigé ; shims musique / intro / palette Android | - -### Correctif clavier (ce jalon) -- **Problème :** le clavier n’apparaissait pas sur « Nom du joueur » ; une fois affiché, impossible d’effacer le `PLAYER` prérempli. -- **Cause :** `window.lua` appelait les globaux `SDL` / `TH` (inexistants — modules en `require` local), donc l’IME n’était jamais demandé. Le `SurfaceView` immersif volait aussi le focus à `DummyEdit` SDL. -- **Correction :** - - `require("TH")` / `require("sdl")` dans `window.lua` ; demande IME à l’activation / tick du textbox - - Pont `EditText` dans `GameActivity` + `CthImeBridge` → texte / retour arrière SDL - - Retour arrière sur buffer IME vide transmis au textbox du jeu - - `showTextInput` SDL privilégie le chemin clavier de `GameActivity` - -### Hors périmètre de cette pre-release -- Phase 8 durcissement Android 14/15/16 -- Phase 9 découplage community / Google -- Phase 10 engineering release / Play Store -- SDL3 / MIDI (reportés) +| 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 -- APK **pre-release**, signé debug sauf si les secrets CI de signature sont fournis. -- Données Theme Hospital toujours requises (assistant / import SAF). -- Installation propre recommandée si une ancienne build debug a laissé un état moteur. +- **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.apk` (arm64-v8a) +- `CorsixTH-Android-0.70.1-pre-release-2.apk` (arm64-v8a, **community**) 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 index 6493ddcbc..efd5a5b98 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -14,91 +14,55 @@ Base branch: ## Current phase -**Phase 7 — Save/load and lifecycle reliability** +**Phase 9 — Community build cleanup** -Status: **DONE** (device smoke + soft keyboard accepted) +Status: **DONE** -Checkpoint: **`v0.70.1-pre-release`** (APK + bilingual notes) +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 (`assembleDebug` + SetupActivity) +- 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 8 — Android 14/15/16 hardening** +**Phase 10 — Release engineering** ## GitHub Actions status -**DO NOT RUN without explicit repository-owner approval.** +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). -Local-first validation only. Self-hosted runner `GAMING` must not be used without approval. +Community CI tasks: `assembleCommunityDebug` / `assembleCommunityRelease`. ## Modernization target - Core: **CorsixTH 0.70.1** -- ABI priority: **arm64-v8a** (now enforced in build) -- Android targets: 14 / 15 / 16 +- ABI priority: **arm64-v8a** (enforced) +- Android targets: **compile/target SDK 36** (Android 16) - Multimedia: **SDL2** (SDL3 deferred) +- Flavors: **community** (default) / **playstore** (optional) -## Phase 3 summary - -Deliverable: [`docs/PHASE3_NATIVE_TOOLCHAIN.md`](PHASE3_NATIVE_TOOLCHAIN.md) - -| Item | Result | -|---|---| -| C++17 | `APP_CPPFLAGS` + engine `LOCAL_CPPFLAGS` | -| STL / platform | `c++_static` / `android-27` | -| ABI | **`arm64-v8a` only** (`Application.mk` + `ndk.abiFilters`) | -| NDK | 27.0.12077973 | -| SDL2 | retained (2.30.10) / mixer 2.6.3 | -| PNG strategy | keep lodepng for first 0.70.1 attempt; prefer libpng later | -| MIDI | deferred | -| Submodule | Phase 4 branch `feature/android-phase4-0.70.1` (0.70.1 + Android glue) | -| Validation build | **SUCCESS**; APK ~43.9 MiB arm64-only | - -## Phase 4 summary - -Deliverable: [`docs/PHASE4_BOOTSTRAP_0.70.1.md`](PHASE4_BOOTSTRAP_0.70.1.md) +## Phase 9 summary | Item | Result | |---|---| -| Core | CorsixTH **0.70.1** sources imported | -| Android glue | hooks/events + Lua touch/settings restored | -| Native | `libappmain` builds (lodepng PNG; MIDI device off) | -| Device | GameActivity **Resumed** after `fixConfig` nil-env fix | -| Push | not done (local commits only when requested) | - -## Phase 5 summary - -Deliverable: [`docs/PHASE5_DATA_PACKAGING.md`](PHASE5_DATA_PACKAGING.md) - -| Item | Result | -|---|---| -| `game.zip` | 0.70.1 layout; Bitmap build helpers excluded | -| Engine upgrade | markers + `api_version` **2717** + `.cth_engine_stamp` | -| TH import | SAF persistable; nested-root resolve; post-import validate | -| Setup wizard | confirmed when TH missing | -| Game with TH | GameActivity resumes; data found after restart | - -## Phase 6 summary - -Deliverable: [`docs/PHASE6_GAMEPLAY_ESSENTIALS.md`](PHASE6_GAMEPLAY_ESSENTIALS.md) - -| Item | Result | -|---|---| -| Touch / scroll | Phase 4 synthesis retained | -| Capture mouse | forced off for Android | -| Back button | trapped → Escape/cancel | -| Save hooks | menu-bar swap + save DB update | -| Play / stats | Android events + Play menu restored | -| Device smoke | pending (ADB offline at close) | +| 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 @@ -112,19 +76,17 @@ Deliverable: [`docs/PHASE6_GAMEPLAY_ESSENTIALS.md`](PHASE6_GAMEPLAY_ESSENTIALS.m | 5 | Data packaging/import | DONE | | 6 | Input/UI/audio/gameplay essentials | DONE | | 7 | Save/load/lifecycle reliability | DONE | -| 8 | Android 14/15/16 hardening | NOT STARTED | -| 9 | Community build cleanup | NOT STARTED | +| 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 8** — Android 14/15/16 hardening (scoped storage, exported components, etc.). +**Phase 10** — Release engineering (reproducible package, signing docs, checksums, changelog). Do **not** enable MIDI or SDL3 yet. -CI: GitHub Actions must use self-hosted runner **GAMING** (`runs-on: [self-hosted, Windows, X64]`). - ## Soft keyboard (post Phase 7) - IME shown for player name / textboxes via `GameActivity` EditText bridge @@ -138,9 +100,5 @@ CI: GitHub Actions must use self-hosted runner **GAMING** (`runs-on: [self-hoste - `docs/AI_AGENT_RULES.md` - `docs/PROJECT_STATUS.md` - `docs/MIGRATION_MATRIX_0.70.1.md` -- `docs/PHASE3_NATIVE_TOOLCHAIN.md` -- `docs/PHASE4_BOOTSTRAP_0.70.1.md` -- `docs/PHASE5_DATA_PACKAGING.md` -- `docs/PHASE6_GAMEPLAY_ESSENTIALS.md` -- `docs/PHASE7_SAVE_LOAD_LIFECYCLE.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 37a26f9a9..40d76520a 100644 --- a/jni/Application.mk +++ b/jni/Application.mk @@ -12,3 +12,7 @@ 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/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/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/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 4fa7852da..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,12 +48,7 @@ class CTHApplication : android.app.Application() { override fun onCreate() { super.onCreate() - // Community/debug builds use a stub google-services.json and are not - // linked to the Play Console project; initializing Play Games there only - // shows a useless sign-in UI and can confuse first-launch validation. - if (!BuildConfig.COMMUNITY_BUILD) { - 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 80b03a43c..229977dfe 100644 --- a/src/Java/uk/co/armedpineapple/cth/GameActivity.kt +++ b/src/Java/uk/co/armedpineapple/cth/GameActivity.kt @@ -23,8 +23,6 @@ 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 @@ -64,7 +62,7 @@ class GameActivity : SDLActivity(), Loggable { StatisticsService((application as CTHApplication).statsDatabase) } - private var playGamesService: PlayGamesService? = null + private var playGamesService: PlayGamesController? = null /** Real EditText used to attach the soft keyboard (SurfaceView steals DummyEdit focus). */ private var imeEdit: EditText? = null @@ -85,9 +83,8 @@ class GameActivity : SDLActivity(), Loggable { override fun onCreate(savedInstanceState: Bundle?) { singleton = this - if (!BuildConfig.COMMUNITY_BUILD) { - playGamesService = PlayGamesService(this, statisticsService) - } + AndroidUiHardening.applyGameDisplayCutout(this) + playGamesService = DistributionBootstrap.createPlayGames(this, statisticsService) val filesService = FilesService(this) @@ -539,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