# Online character persistence (save.online) ## What it is for `save.online` is a thin adapter between a game and an authoritative save host. The host side (`save.backend`) owns the credentials and the HTTP calls; this kit queues the requests a game submits, polls the backend every update, and hands each answer back exactly once. It also keeps typed character profiles (one per account, bound to a campaign actor, with experience, inventory and a zone) and makes sure a boss reward is granted only once per receipt. It has no display module and no rules of its own about what a character is worth: the starting zone, the level thresholds and the rewards are your configuration. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "save.online" } ``` The kit requires `game.engine` and provides the exclusive service `save.online`. A `save.backend` service must be present in the host that runs the game; without it a submit fails at once and nothing stays pending. The kit has no settings of its own: progression is configured through the `configure` command below. Do not put credentials in any configuration. ## First example Submit a read, wait for the backend to answer, take the answer once: ```text id = save.online.submit({ "op": "read", "account": "a", "character": "c" }) // a request id save.online.pending() // [id] until the backend answers save.online.take(id) // null while pending // ... after the backend answers, the event save.online.completed is emitted save.online.take(id) // { "ok": true, "value": { ... } } save.online.take(id) // null: the answer was consumed ``` ## Services - `save.online` (service), methods: - `submit(command)`: sends a command to `save.backend` and returns its request id. Only the operations `read`, `commit` and `commitBatch` are accepted. At most 64 requests may be pending or waiting to be taken; take results before submitting more. - `take(request)`: returns the completed answer once and forgets it; `null` while the request is pending or unknown. A backend failure completes the request as `{ "ok": false, "error": "..." }`. - `pending()`: the ids still waiting for the backend. - `character(command)`: typed profile operations, see below. ## Events - `save.online.completed` (event): emitted once when the backend answers a submitted request. Payload `{ request, result }`; the result is the backend's own document, to be read with `take`. ## Character commands `character(command)` takes an object with an `op` (unknown fields are refused): | `op` | Arguments | Result | |---|---|---| | `configure` | `config` with `startZone`, `levelThresholds`, `rewards` | `null`; set before binding profiles, a different config afterwards is refused | | `roster` | `actors` (up to 40 distinct ids) | `null`; the campaign actors profiles can be bound to | | `bind` | `account`, `character`, `name` | `{ actor, zone }`; one character per account, a repeated bind returns the existing one | | `hydrate` | `account`, `character`, `revision`, `data` | `null`; loads a document read from the backend | | `zone` | `character`, `zone` | `null`; moves the character, marking it unsaved | | `reward` | `character`, `receipt: [instance, generation, boss, actor]` | `{ granted, experience, level, inventory }`; `granted` is `false` for a receipt already seen | | `award` | legacy form with `experience`, `item`, `count` | same as `reward` | | `view` | `character` | name, zone, experience, level, inventory, revision, `saving` | | `actor` | `actor` | the character id bound to that actor, or `null` | | `list` | none | every `{ account, character, actor }` | | `pending` | none | profiles not yet persisted: `account`, `character`, `expectedRevision`, `data` | | `ack` | `revisions` (character to new revision) | `null`; marks profiles persisted when each revision is exactly the next one | Configuration limits: `levelThresholds` is 1 to 100 strictly increasing numbers starting at 0 (default `0, 100, 200, ...`), `startZone` defaults to `crypt-1`, `rewards` maps a zone to a boss to `{ experience, items }` (up to 64 zones, 32 bosses per zone, 128 items per reward). The usual loop: `pending` gives the documents to persist, you send them with `submit` as a `commit` whose `expectedRevision` is the one returned, and when the answer is a success you call `ack` with the new revisions. ## Collections - `online_worlds`: authoritative online world documents (characters, zones and rewards) kept through the backend. ## Save and rollback The roster, the configuration and the profiles are saved with the game, and a reload keeps rewards from being granted twice. Pending requests, completed answers and credentials are never saved: after a reload `pending()` is empty. A game without a roster saves nothing from this kit. ## When it does not do what you expect - `submit` fails with "only read/commit/commitBatch may be submitted": other operations (save, load, list) are not allowed. - `submit` fails with "consume completed results before submitting more": 64 requests are pending or untaken; call `take`. - `submit` fails right away: no `save.backend` is installed in the host. - `take` returns `null`: the backend has not answered yet, or the answer was already taken. - `reward` returns `granted: false`: that receipt was already applied. An error "receipt recipient mismatch" means the actor in the receipt is not the one bound to the character. - `configure` fails with "progression configuration mismatch": profiles already exist; changing the progression needs an explicit save migration. - `bind` fails with "persistent campaign roster full": every actor of the roster already has a character. ## Limits - It does not speak HTTP and holds no credentials; the backend does. - No gameplay rollback of network state, and no display. - Only read and commit style requests can be submitted. ## Versions Kit version 1.0.0, the first delivery of the online character profiles. It relies on `game.engine` 1.x.