A fifth tab on the Rust server screen, between Online and Wipes, shown when the site's module declares `map` under the app's one capability rule (D122). `?tab=map` opens it; on a site without a map it opens the feed. The phone draws the map itself (D121): Coil loads the picture once per map as RGB_565, and a Compose Canvas draws the grid and the markers, with pinch, pan and double tap. The frame is §30.3's transform, ported from the web's mapGeometry.js with its test cases. Markers keep their size on screen; a zone is drawn on the ground. Nothing here gates a layer: map/live is projected per viewer on the server, and the app draws what it is sent. - The legend: a switch per layer the viewer was sent, and who can see each one they were not (with the D113 line). The mates row offers the Steam link to a signed-in, unlinked viewer. - The tap card: what a marker is, its grid square, and what its layer carried. A site-event marker whose run core's public calendar lists (D125) has Open event, which opens the app's own event page on that run (D123). A rehearsal's or unlisted event's marker says only "Site event". - The poll: map/live every pollMs (10 s) while RESUMED on the Map tab (D124), and not at all for a viewer sent nothing that moves. A new mapKey re-reads the map, so a new seed's picture follows in place. - Per account: the map's state is dropped and re-asked on a change of signed-in account, and an answer asked for the previous account is discarded when it lands. Found on the emulator walk and fixed here: the reader's zoom was lost on the way back from the event page, and again whenever the status line under the map changed length (a resize was treated as a rotation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Runic Gateway — Android app
A native Android client for a Runic Gateway shard's public site + player self-service. It is
purely an API client of the website backend — it never talks to the link/ sidecar or the game
shard directly, and it ships none of the shard/sidecar wiring. It surfaces the same content and
player features as the website's browser client, minus every administrative/management console.
The authoritative design contract is docs/android/PLAN.md
in the RunicGateway/docs repo. The authoritative API reference is the committed OpenAPI spec at
website/server/swagger/swagger-output.json.
Status
M0 — repo scaffold. Gradle + Compose + Hilt skeleton with CI (lint + unit test + debug build). The functional Kotlin pass (M1–M4) and the design pass (M5) follow — see the plan's milestones (§9).
Stack
| Concern | Choice |
|---|---|
| Language / UI | Kotlin + Jetpack Compose (Material 3) |
| Navigation | Navigation-Compose, single-activity |
| HTTP | Retrofit + OkHttp, kotlinx.serialization |
| Async | Coroutines + Flow |
| DI | Hilt |
| Prefs / base URL | Jetpack DataStore |
| Tokens at rest | EncryptedSharedPreferences |
| Images | Coil |
| Min SDK | Android 10 (API 29) |
| Target / compile SDK | 35 |
Dependency and plugin versions are pinned in gradle/libs.versions.toml.
Build
Requires JDK 17 and the Android SDK (ANDROID_HOME / local.properties).
./gradlew assembleDebug # build a debug APK -> app/build/outputs/apk/debug/
./gradlew test # JVM unit tests
./gradlew lint # Android lint
./gradlew installDebug # install on a connected device/emulator
The app self-configures its server URL on first run (PLAN.md §3), so a single build works against any shard's website — there is no compiled-in API host.
CI
.gitea/workflows/pr-checks.yml gates PRs into main and edge with
./gradlew lint test assembleDebug on the org's self-hosted runner (JDK 17 + Android SDK). Debug
builds are auto-signed, so the gate needs no secrets. This pipeline is verified green end-to-end on
the runner (M0). A signed release APK attached to a Gitea release comes at M6.
edge is in the trigger deliberately: a workstream that lands its phases on a working branch
before one cutover PR into main otherwise gets no CI at all until the cutover — which is what
happened to all nine M12 phase PRs (docs/website/ENGAGEMENT.md §7.1 Q8). sonarqube.yml is
unaffected: it is a push-on-main analysis, not a PR gate.
The workflow carries a few runner-specific accommodations (each explained in comments in the file), because this self-hosted runner differs from a stock GitHub runner:
- JDK 17 is installed via
apt(notactions/setup-java) — the runner can't resolveapi.adoptium.net, while the Ubuntu mirrors are reachable. - SDK packages are installed explicitly via
sdkmanager, withset +o pipefailsoyesdying ofSIGPIPEdoesn't fail the step. gradlewischmod +x'd in the run step — the runner's checkout does not preserve the git executable bit, so./gradlewalone fails with "Permission denied".
Contributing
See CONTRIBUTING.md. AI-assisted contributions must be disclosed (org
policy): tick the PR box naming the tool and add a Co-Authored-By trailer to AI-authored commits.
Licensed GPL-3.0-or-later.