M11 Part 2 (docs/android/PLAN.md §9), on the visibility plumbing Part 1 added.
Each screen hides from the menu when the shard doesn't publish its feature, and
self-reports "not available here" from its own 404/403 so a deep link still
lands on an honest answer.
- Rules (/public/shard/ruleset). A null body means the shard has never
published a ruleset, which is a SUCCESS state, not the feature being off —
the screen tells the two apart. Blocks render only when published, since an
omitted block means the system is off rather than unknown. Skill caps are
converted out of tenths; the raw 1000 reads as ten times the real limit.
Live via world.ruleset, which the shard re-emits on every reconnect.
- Leaderboards (/public/shard/points). Boards order most-contested first, live
via points.board. maxPoints 0 is uncapped so no cap line is drawn, and a
cliloc-named board (nameString null, the usual case) falls back to the
humanised PointsType key. A nameless rank is a valid row: the character name
is the feature's one admin-configurable field.
- Market (/public/shard/market + /meta + /vendors/:serial). NOT live: the
market feature ships with its SSE fan-out disabled, so this is a plain
paginated read, searched on submit rather than per keystroke because it is
the site's first rate-limited public endpoint. The staleness line is
required, not decoration — the round-robin sweep means a price can be a full
cycle old. The vendor screen is the only surface that can render a truncated
shop and a gated location, the latter as a real answer rather than a blank
coordinate.
- Atlas (/public/atlas/creatures[/:slug]). Static shard content, so it stays
readable while the shard is down — but site-mode gated, unlike /shard/*.
Rows lead with the server's placement label ("Despise, Felucca"), which is
the transform the whole feature exists for. Respawn delays are read as
SECONDS, the unit the parser normalises XmlSpawner's mixed minutes/seconds
into. Facet filter options are discovered from the shard's own data — nothing
here names a facet, since a shard may add, replace or rename them.
336 unit tests pass (32 new); lint clean. The five-rung on-device walk runs
against a local website on the cutover branch before the cutover merges.
Co-Authored-By: Claude <noreply@anthropic.com>
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 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.
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.