Back to Black Site
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.

docs/account-sync.md
# Player account synchronization

Implemented on `plan/armory-sync`, alongside the player identity menu. Open Player Account to enable each sync feature and save a batch of pending changes. Account data loads automatically on sign-in and after returning from a mission. Saves use the connected player's identity and trusted runtime approval. The game never receives a signing key or accesses relays directly.

## Two independent application records

| Feature | Collection | Record ID | Schema | Data |
| --- | --- | --- | --- | --- |
| Armory progression | player-armory | progress | blacksite.armory, version 1 | Unlocked throwables, owned armor, weapon and armor upgrade levels |
| Player customization | player-customization | choices | blacksite.customization, version 1 | Character, primary/secondary weapon, throwable, equipped armor, difficulty and map |

Both are NIP-78 kind 30078 records using the documented `soy.app-data/1` envelope and host-provided stable scope. Their addresses are `30078:<player>:soy.app-data/1:<scope>:<collection>:<record-id>`. The same verified game scope and compatible configured storage relays are required on each device/runtime. Preview, proposals, remixes and different creators need not have the published game's scope or write policy. Cross-runtime portability depends on the documented host app-data policy; OUTBOX alone is insufficient.

Example customization `data` (the complete event uses the same six-field envelope and six tags documented in [napplet-data.md](napplet-data.md)):

```json
{
  "character": "sniper",
  "loadout": { "primary": "ak47", "secondary": "deagle", "throwable": "smoke" },
  "equippedArmor": "soft",
  "difficulty": "easy",
  "map": "docks"
}
```

All IDs and level limits are derived from the shipped game catalog at build time. Readers reject incompatible versions, unknown IDs, invalid levels, unowned armor upgrades and malformed envelopes. A complete read of the current revision precedes any publication. Incomplete reads and tombstones never authorize a reset or replacement. Funds, campaign, best payout and active mission state remain local.

## Merge and ownership behavior

Permanent ownership merges by set union; upgrade levels merge by maximum. A second device does not relock something already purchased. These are player-owned progression records, not an authoritative economy ledger.

Customization is a single latest-saved snapshot. A clean local save restores the account's choices. Unsaved local changes are retained during a read, and an explicit save uses those choices against the newly observed revision. Conflicting writes are rejected by the host's stale-base checks where available; the next user retry rereads and prepares against the current base. Independent writers can still race across relays: the ordinary addressable-event ordering applies. Uncertain retries retain the same prepared template while the session remains open.

Local NAP-STORAGE keys are isolated by player public key. Original `blacksite.*` saves remain the guest/device save. They are never silently uploaded to the first signed-in account. “Use this device's armory” merges only permanent progression; “Use this device's choices” imports only customization. Neither imports the guest's funds or campaign. The import decision and dirty customization state persist separately per account.

An account switch invalidates pending reads, merges and publications. A running mission retains its original local save owner; the new player's save activates after the mission, under a short loading overlay. Incoming remote updates also wait until a safe menu boundary. Equipped armor arriving before its ownership record remains unequipped until ownership is available; settings cannot grant free armor.

## Optional runtime support and UI

Identity, storage, OUTBOX and the documented host app-data hint are optional for the game. All ordinary calls use SDK helpers; `armory-host.ts` is the sole adapter observing the already injected initialization hint through the documented helper. No new NAP or app-owned handshake is added.

An unsupported runtime displays one small warning in Player Account (and in the Armory): “This runtime does not support the features needed for sync.” No sync action buttons or technical protocol terminology are shown. Signed-out supported runtimes ask the player to sign in without actionable save controls. Transient connection failures and denied approvals keep device progress and expose recovery only when the runtime supports it.

The enabled controls explain that these records are public and each save asks for approval. Sync is an approved batch action, not silent background signing. Save acknowledgement means the selected storage relay accepted the snapshot; it does not establish permanent availability everywhere. New purchases made during a publication remain pending. Device storage failures are surfaced, and unreadable existing local saves are not overwritten with defaults.

## Implementation surfaces

- `armory.ts` / `customization.ts`: catalog-derived validation, canonical snapshots and armory merging.
- `save-store.ts`: isolated local caches, serialization of writes, optional storage and import source.
- `armory-host.ts`: SDK-backed access to the existing application-data helper.
- `armory-sync.ts` / `customization-sync.ts`: identity generation guards, reads, merges, safe application, approvals and retry state.
- `armory-ui.ts`: native Player Account and Armory controls and unsupported-runtime warning.
- `game-bridge.js`: appended inside the classic game's scope by the existing Vite plugin; copied game sources remain unchanged by this feature. The bridge survives `scripts/sync-game.mjs` imports.

## Verification and limits

`pnpm test:guidance` includes model/controller tests for merge rules, account isolation, pending purchases during publication, deferred updates, partial reads, denied saves, local corruption and customization conflicts. `pnpm test:conformance` runs both browser suites after its protocol checks.

`tests/account-sync-ui.mjs` runs two independent opaque-origin game frames against a controlled host/relay fixture. It exercises actual purchases, deliberate guest import, two different record addresses, restoring all selected game options, keeping funds local, denied saves, incomplete reads, incompatible revisions, tombstones and switching accounts during a mission. It establishes repository integration, not live relay durability or real signer approval.

The actual soyLI hosted preview was tested signed out: it displayed the unsupported-runtime warning with zero visible sync actions. Menu/Help/Player navigation, a user-triggered SDK storage write, 200×160, 320×560, 560×380, 900×600, 1440×900, 390×844, 360×640 and 844×390 frames were checked. Explicit test-only iframe dimensions were used; small frames retain the existing 560×380 minimum notice. Touch menu navigation was emulated, and app colors remain owned by the game.

Real connected-account approval, two deployed compatible runtimes with the same game scope/storage relays, actual relay persistence and physical phone testing remain unverified. No gameplay save was published using the creator's CLI account.