161 lines
6.5 KiB
Markdown
161 lines
6.5 KiB
Markdown
# Local-first content updates and Android releases
|
|
|
|
Healer Man separates the installed game client from large game content. The
|
|
Android APK contains the React/Three.js runtime, native Android bridge, menus,
|
|
local profile and save support, UI assets, and the Wailing Caverns starter
|
|
pack. Other GLBs, textures, audio, character packs, dungeons, and Manastorm
|
|
stages are published by TrueNAS as optional content.
|
|
|
|
Single-player is local-first. Starting the game, loading a character, entering
|
|
installed content, simulating combat, and saving progress never require the
|
|
account server. Online authentication, cloud synchronization, rankings, and
|
|
content discovery are opportunistic services rather than gameplay
|
|
dependencies.
|
|
|
|
## Update boundaries
|
|
|
|
Content-only changes do not require a new APK when the installed client already
|
|
understands the data:
|
|
|
|
- GLB additions or replacements
|
|
- textures, icons, audio, and previews
|
|
- data-driven encounter, loot, dialogue, or balance data
|
|
- new files added to an existing compatible content pack
|
|
|
|
A new Obtainium APK is required for changes to JavaScript/React gameplay code,
|
|
UI behavior, shaders or loaders, native Android code, permissions, or the
|
|
content-manifest schema. A manifest can set `minimumAppVersion` so an older APK
|
|
will not activate incompatible content.
|
|
|
|
The browser build remains a full build served by TrueNAS. It receives client
|
|
code when the app checkout is pulled, rebuilt, and restarted. The Android build
|
|
is deliberately separate and compact so the APK does not inherit the complete
|
|
multi-gigabyte browser asset catalog.
|
|
|
|
Measured on August 14, 2026, the production browser `dist` is 3.88 GiB across
|
|
18,895 files; GLBs account for 3.75 GiB. The compact Android `dist-android` is
|
|
148.0 MiB across 5,561 files, including 47.8 MiB of starter GLBs. The final
|
|
signed APK will be smaller because Android packages compress eligible files;
|
|
record its exact size after the first release build.
|
|
|
|
## Content manifest
|
|
|
|
`npm run content:publish` scans runtime content, calculates SHA-256 for every
|
|
file, writes immutable objects, and publishes `/content/manifest.json` last.
|
|
Each manifest entry contains:
|
|
|
|
- a stable logical path used by the client
|
|
- an immutable object URL derived from the file hash
|
|
- the SHA-256 digest and byte count
|
|
- the logical pack that owns the file
|
|
|
|
The runtime continues to request stable paths such as:
|
|
|
|
```text
|
|
/assets/game/dungeons/wailing-caverns/wailing-caverns-visual.glb
|
|
```
|
|
|
|
The content resolver maps that path to a verified file in Android app-private
|
|
storage. Filenames are therefore not used as version numbers and runtime code
|
|
does not need to know the hash.
|
|
|
|
## Safe publication and activation
|
|
|
|
Publication order is intentionally atomic:
|
|
|
|
1. Hash the source files.
|
|
2. Copy any new immutable objects into the content volume.
|
|
3. Write the versioned manifest.
|
|
4. Replace `manifest.json` only after all referenced objects exist.
|
|
|
|
On a device, the client fetches the small manifest and compares it with its
|
|
installed manifest. Only missing or hash-mismatched objects are downloaded.
|
|
Android streams each object to a `.part` file, supports HTTP range resume,
|
|
verifies its size and SHA-256, then renames it into the object cache. The active
|
|
logical-path map changes only after the complete pack verifies. An interrupted
|
|
or corrupt update leaves the previous pack playable.
|
|
|
|
The starter pack is bundled into the APK. Downloaded packs live under Android's
|
|
app-private files directory, outside the packaged web bundle, and survive an
|
|
APK update. Players may remove optional packs through the content manager; save
|
|
data is stored separately and is never removed with content.
|
|
|
|
## Offline behavior
|
|
|
|
- The app never blocks startup on a network request.
|
|
- Bundled and previously installed packs remain playable offline.
|
|
- The last verified content map is read synchronously from local storage.
|
|
- A failed update check is silent outside the content manager.
|
|
- A failed or interrupted download never replaces active content.
|
|
- Local saves are authoritative during play and cloud synchronization retries
|
|
when connectivity returns.
|
|
- Content that has never been downloaded is clearly marked as unavailable
|
|
offline instead of loading a broken scene.
|
|
|
|
Loading the game client itself from a remote WebView is intentionally avoided.
|
|
Keeping the runtime in the APK is what makes offline startup reliable.
|
|
|
|
## TrueNAS layout
|
|
|
|
```text
|
|
/mnt/usbssds/apps/healer-man/app Runnable Git checkout
|
|
/mnt/usbssds/apps/healer-man/data SQLite accounts and cloud saves
|
|
/mnt/usbssds/apps/healer-man/content Published manifests and immutable objects
|
|
/mnt/usbssds/apps/healer-man/backups Database backups
|
|
```
|
|
|
|
The Node server exposes the content volume at `/content`. Manifests use
|
|
`no-cache`; immutable hashed objects use long-lived cache headers and support
|
|
byte ranges for resumed downloads.
|
|
|
|
After pulling source on TrueNAS, publish content before restarting the app:
|
|
|
|
```sh
|
|
CONTENT_DIR=/mnt/usbssds/apps/healer-man/content \
|
|
npm --prefix /mnt/usbssds/apps/healer-man/app run content:publish
|
|
```
|
|
|
|
Content objects are uploaded before `manifest.json`, so a client can never
|
|
discover a manifest whose files have not been published.
|
|
|
|
## Pack policy
|
|
|
|
Packs are player-facing groups while files remain individually addressable.
|
|
This permits a fast initial pack install and small later patches. Current pack
|
|
IDs are derived predictably:
|
|
|
|
```text
|
|
dungeon:wailing-caverns
|
|
dungeon:forsaken-abbey
|
|
characters:wow335a-v1
|
|
characters:ascension-v1
|
|
characters:runewaker-v1
|
|
manastorm:<stage-id>
|
|
shared:creatures
|
|
```
|
|
|
|
Changing one GLB changes one object hash. The client downloads that object and
|
|
atomically updates the pack rather than downloading every dungeon again.
|
|
|
|
## Obtainium release policy
|
|
|
|
Obtainium monitors the Gitea Releases page. APKs are release attachments, not
|
|
files committed to `main`. Every APK release must:
|
|
|
|
- increase Android `versionCode`
|
|
- use a semantic `versionName` and matching Git tag
|
|
- be signed with the permanent Healer Man release key
|
|
- include a SHA-256 checksum attachment
|
|
|
|
Content publication is independent from this process. Routine asset updates
|
|
can be published immediately without creating an APK release. Client or native
|
|
changes use the signed APK workflow documented in `DEPLOYMENT.md`.
|
|
|
|
## Compatibility and rollback
|
|
|
|
Manifests have a schema version and `minimumAppVersion`. The client refuses an
|
|
unsupported schema or incompatible minimum version. Immutable old objects are
|
|
not overwritten, so the previously active manifest remains a valid rollback.
|
|
Content-volume cleanup must retain all objects referenced by the current and
|
|
previous production manifests.
|