SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
# Armory account sync — proposal plan
Status: implemented on this branch; see [account-sync.md](account-sync.md) for the shipped behavior and verification. Original planning context: player-menu foundation is commit `f41232c` on `pr/menu-nostr-identity`. The implementation also adds a separate player-customization record at the user’s request. The user confirmed that all permanent armory progression is in scope: throwable unlocks, owned armor, weapon upgrade levels and armor upgrade levels.
## Player outcome
Signing into the same game account on another computer restores permanent armory progression. Purchases remain usable immediately and save locally even when the account, host, or relay is unavailable. The game UI says “Armory sync”, “Saved to account”, “Changes saved on this device” and “Sync unavailable”, without protocol terminology.
The signed-in player's public key comes from the identity controller. Never use the CLI creator/publisher identity for gameplay saves. No hard NAP requirements are added: local play remains available.
## Protocol and host prerequisites
NIP-78 supports addressable application data in kind 30078 with a context in `d`. It recommends authenticated owner access; actual relay behavior matters. Reference: [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md). Event identity is kind + author + d; newest revisions and equal-timestamp ordering follow [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md).
Use the project's shipped public `soy.app-data/1` convention from [napplet-data.md](napplet-data.md) and the existing app-data helper/contract. It is public, unencrypted progress data under the current host policy, not a private cloud-save guarantee. Explain public progress storage when the player enables sync. No private data or profile metadata belongs in the progress payload.
Read and publish through OUTBOX and the documented helper; never add relay pools, direct network access, signing keys, encryption or a new NAP. OUTBOX presence alone does not grant publishing permission. The helper observes the injected host's existing `shell.onReady` policy hint; it does not initiate a handshake. This is a soyLI host extension, not a new universal SDK domain. Preserve its documented adapter boundary instead of inventing an app-owned shell client.
Implement only after confirming these prerequisites:
- Both target shells expose the compatible app-data write policy and preserve the same verified stable game identity/scope. The helper derives `d` from the host's scope; the app must not assert or overwrite it.
- Both devices select compatible storage relays containing this player's record. The host owns configured destinations and signing consent. Do not hardcode relay.napplet.soy as the only possible destination, silently add destinations, or presume NIP-65 discovery finds the app-data storage relay.
- Local preview and published playback intentionally have different scopes. Tests must compare two sessions of the same published game, not a preview against a publication. Release hashes must not change record identity. Remixes/different creators may have separate scopes.
- Proposal playback may remain guest-only under the current host policy. A playable proposal fixture is insufficient evidence for production signed-in writes.
- A generic shell lacking this policy can continue local play, but cross-shell writes are not established until a compatible adapter/grant is shipped. Report the capability gap rather than changing namespaces or bypassing host policy.
## Record contract
Proposed stable collection: `player-armory`. Record ID: `progress`. Schema label: `blacksite.armory`. Version: `1`. These are application identifiers, not NAP names. Keep them stable across releases.
Address: `30078:<player-hex-key>:soy.app-data/1:<verified-host-scope>:player-armory:progress`.
Use the helper's six standard tags (`d`, `s`, `c`, `L`, `l`, `v`) and exact six-field content envelope. Do not publish a bare `d=blacksite.armory` template, which the current constrained host grant does not permit.
```json
{
"schema": "blacksite.armory",
"version": 1,
"title": "Black Site armory",
"previous": null,
"deleted": false,
"data": {
"unlockedThrowables": ["frag"],
"ownedArmors": [],
"weaponLevels": {},
"armorLevels": {}
}
}
```
For updates `previous` is the observed current event ID; creation uses null only after a complete successful read establishes absence. Actual payload values must reflect the player's purchased progress; the example is not a default unlock grant.
Map fields to today's game profile:
| Event field | Game field | Merge |
| --- | --- | --- |
| unlockedThrowables | PROFILE.unlocked | Set union |
| ownedArmors | PROFILE.armours | Set union |
| weaponLevels | PROFILE.levels | Maximum level per weapon |
| armorLevels | PROFILE.armourLevels | Maximum level per owned armor |
Validate known catalog IDs, unique bounded arrays, plain maps, integer levels within the game's current limits (weapon 0–20, armor 0–10), and armor upgrade/ownership consistency. Canonicalize order and omit zero levels for stable change detection. Reject unknown schema versions and incompatible catalog data before applying or publishing; never overwrite an unsupported current revision with older understood data. Keep the envelope within the host's 16 KiB content limit.
Funds, equipped armor, loadouts, consumables in a mission, campaign, best payout, difficulty and selected character remain local. `armourBonus` is currently initialized but has no armory purchase mutation; do not export it without separate semantics. Unlock-only synchronization does not reconcile the bank spent on purchases across computers. Player-signed progress is portable self-reported state, not an authoritative anti-cheat ledger.
## Local ownership and migration
The existing `blacksite.profile` holds bank and armory state together and does not currently isolate players. Add a migration before attaching any local progress to an account.
Store account-specific profile/cache/pending-sync records through NAP-STORAGE, keyed by player pubkey and schema. Maintain a separate guest save. Do not auto-upload an unowned existing save to whichever player happens to sign in first: offer a one-time “Use this device's armory for this account” import, keep the original save, and remember the decision for that account.
Account change cancels pending profile/armory reads, image reads, subscriptions and prepared publications; responses are guarded by account and generation. Clear account-specific display state immediately. Never merge A's armory into B's profile. When a switch occurs during a mission, keep that mission's save owner fixed, pause any sync, and activate the new account's game save safely on returning to the menu. Guests cannot publish.
## Read, merge and publication flow
1. Identify the player and load their owned local save/cache. Show local armory immediately; network work is bounded and does not block deployment. Capture the local armory before a read so progress earned during the request is retained.
2. Check the app-data policy and initialize the collection helper. Read `get(playerKey, 'progress')` from the host-approved storage relays. Check author, scope, envelope, revision, deletion state, payload, errors and `incomplete`.
3. Merge valid remote progression with the current local armory using union/max. Persist the result locally and notify the game to rerender armory/loadout menus at a safe menu boundary. Avoid replacing active combat state. Do not re-deduct bank funds for already purchased remote unlocks.
4. Route successful armory purchases through an explicit progression-change hook rather than publishing on every `saveProfile()` (which also saves earnings, equipped armor and other state). Save locally first, compare a canonical armory digest, and mark an account-owned pending snapshot.
5. Batch changes into one pending snapshot. Default first slice: automatic restore on sign-in/menu return; enable sync and a “Save armory to account” action in the player-account/armory UI submit pending changes through trusted host approval. The current host requires approval for each publication, so do not promise silent background auto-save or repeatedly prompt after every purchase. An automatic write mode needs an existing host permission model and explicit product choice.
6. Before publishing, re-read a complete current base, merge again, and call `prepare`/`publish` with that base. Skip identical content. If the player adds another unlock while publishing, keep the newer pending snapshot and mark only the acknowledged snapshot saved.
7. On stale-base rejection, re-read/merge and prepare a new change. On an uncertain result, retain the prepared object and pending local state; retry the same prepared publication as the helper supports, then reconcile with the current remote event. Bound retries and expose a player action rather than an endless prompt loop.
8. After acknowledgement, retain event ID/base/digest locally; “Saved to account” means an approved storage relay acknowledged that snapshot, not universal or permanent relay storage. On failure/denial show local-only state and retain pending progress. A partial/incomplete read cannot establish absence or authorize a replacement.
Union/max is monotonic for ordinary progression and converges when both device states are eventually observed and republished. `previous` is a conflict hint, not an atomic compare-and-swap across relays. Two devices can race and one addressable revision can be discarded; retain local pending/cached progression and remerge on the next sync. Do not claim lossless distributed history from one replaceable event. Cross-device reset/deletion is excluded from v1: a tombstone must not be silently resurrected by cached union/max data.
## Implementation slices
1. Confirm two-shell policy/scope/relay compatibility; document the versioned contract, ownership migration and public-data consent.
2. Extract catalog/armory projection, strict validation and pure merge functions with tests for union/max, limits, incompatible data and preservation of local funds/equipment.
3. Add explicit game hooks for armory changes and safe applying/rerendering of imported progression. The synced game's source lives outside the napplet wrapper; preserve the sync script's reproducibility and avoid patching generated game files without maintaining their source/adapter.
4. Add account-scoped local saves and a bounded identity-driven sync controller. Adapt the documented app-data helper, keep host-policy observation isolated, and implement stale-request, dirty snapshot and retry behavior.
5. Add native player-account/armory sync status, enable/import/save/retry controls and keyboard/touch navigation. Retain app-owned styling and current menu width/minimum notice.
6. Run type/build/guidance/conformance/player-UI tests, then real-host storage and consent scenarios. Commit this as a separate implementation proposal after the player-menu foundation.
## Acceptance evidence
- Publish purchased armory as player A in shell/computer 1; sign into A in an independent shell/computer 2 using the same published game scope/storage relays; restore it after reopening. Verify actual equipped/use eligibility, not only event presence.
- Buy different unlocks on both computers; exercise stale bases, same-timestamp revisions, delayed publication and merge/retry. Keep both devices' progression after eventual reconciliation.
- Switch to B and guest while reads/writes are pending; no A unlocks or pending writes attach to B. A mission retains its original save owner.
- Reject malformed payloads, unknown versions/IDs, oversized records, wrong authors/scopes, tombstones, incomplete reads and mismatched relay configuration. Preserve local state; never publish a reset as recovery.
- Test missing policy/OUTBOX/STORAGE, denied signing consent, offline relays, partial acknowledgements, retry and newer purchases during publication.
- Confirm bank/equipment/campaign remain local, old-save import is deliberate, unchanged armory produces no writes, and image/profile/menu behavior still works.
- Inspect supported frames and minimum notice, both host themes and no THEME, and tap player/help/sync controls in emulated portrait/landscape. Separately report actual phone and production relay coverage.
## Open choices
- Whether host-approved manual batch saves satisfy the first slice or an automatic permission model is required.
- Which second shell and storage-relay configuration establish the requested cross-shell portability.
- Whether future reset or bank synchronization is needed; each requires semantics beyond monotonic unlock merging.
