201 lines
8.6 KiB
Markdown
201 lines
8.6 KiB
Markdown
# Local-first content updates for browser and Android
|
|
|
|
Healer Man separates the installed game client from large game content. Both
|
|
the production browser build and Android APK contain a compact required set:
|
|
the React/Three.js runtime, menus, local profile and save support, UI assets,
|
|
starter equipment, Wailing Caverns starter content, and the basic Human
|
|
character model with its default male and female appearances. Android also
|
|
contains its native bridge. Other GLBs, textures, audio, character/skin packs,
|
|
and dungeons are published by TrueNAS as optional content. Internal Manastorm
|
|
environment packs remain in the catalog only because regular dungeons reuse
|
|
them as automatic dependencies.
|
|
|
|
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.
|
|
|
|
Production builds deliberately stage only required content. `npm run build`
|
|
prepares `.web-public` and emits the compact browser `dist`; Android prepares
|
|
`.android-public` and emits `dist-android`. The development server continues to
|
|
use the source `public` tree so asset-pipeline work remains convenient. The
|
|
complete source catalog and published immutable objects stay on TrueNAS, not in
|
|
every player's initial browser or Android installation.
|
|
|
|
## 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 or, in production browsers, to the app-managed content cache through a
|
|
same-origin service worker. 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.
|
|
|
|
The client fetches the small manifest and compares it with its bundled and
|
|
installed file identities. Only missing or hash-mismatched objects are
|
|
downloaded. Browsers download into a temporary Cache Storage staging area,
|
|
verify every object's byte count and SHA-256, and promote the pack to the active
|
|
cache only after the complete pack succeeds. 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. An interrupted or corrupt update leaves the
|
|
previous pack playable on either platform.
|
|
|
|
The Downloads screen uses the same lifecycle in browser and Android. A missing
|
|
pack offers **Download**, an installed pack with changed files offers
|
|
**Update**, and an installed optional pack offers **Remove**. Required packs are
|
|
marked as included and cannot be removed. Browser packs live in the app-managed
|
|
cache; Android packs live in app-private storage and survive APK updates. Save
|
|
data and roster identities are stored separately and are never removed with
|
|
content.
|
|
|
|
Character packs are presentation-only. When a selected race's pack is absent,
|
|
removed, disabled, or fails to load, the shared character renderer uses the
|
|
matching-gender basic Human model and default appearance. The saved race, class,
|
|
equipment, progression, and appearance choices remain intact and become visible
|
|
again after the pack is installed.
|
|
|
|
Original spell effects and authentic sounds are enabled by default. The
|
|
`core:combat-media` pack contains the original models and textures for every
|
|
mapped WoW class visual, plus authentic sounds. It downloads automatically
|
|
when enabled and stays outside required bootstrap content. Missing visual
|
|
phases use generated effects; missing sounds stay silent. Removing the pack
|
|
returns visual playback to the generated fallback. Run `npm run effects:wow`
|
|
before the normal content publishing workflow to regenerate the original assets.
|
|
|
|
## 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.
|
|
- Missing optional character packs fall back to the basic Human model.
|
|
- Generated spell effects remain available without original spell media.
|
|
- 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 compact production browser client at `/` and the
|
|
complete downloadable-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 compact initial install and small later patches. Required pack
|
|
IDs include:
|
|
|
|
```text
|
|
core:runtime
|
|
core:ui
|
|
core:equipment
|
|
core:character-basic
|
|
dungeon:wailing-caverns
|
|
```
|
|
|
|
`core:character-basic` owns the two basic Human rigs, the character manifest,
|
|
and only the default texture selections needed by the fallback. The remaining
|
|
human customizations and every non-Human character model/skin are optional.
|
|
|
|
Downloadable pack IDs include:
|
|
|
|
```text
|
|
dungeon:forsaken-abbey
|
|
characters:wow335a-v1
|
|
characters:ascension-v1
|
|
characters:runewaker-v1
|
|
core:combat-media
|
|
shared:creatures
|
|
```
|
|
|
|
`manastorm:*` and `core:arenas` remain valid internal pack IDs for compatibility
|
|
but are hidden from the Downloads screen. Installing a regular dungeon still
|
|
installs its exact environment dependency closure automatically.
|
|
|
|
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.
|