6.5 KiB
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:
/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:
- Hash the source files.
- Copy any new immutable objects into the content volume.
- Write the versioned manifest.
- Replace
manifest.jsononly 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
/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:
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:
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
versionNameand 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.