mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-13 04:21:02 +00:00
131 lines
11 KiB
Markdown
131 lines
11 KiB
Markdown
# Capability negotiation and extensions (Phase 7)
|
|
|
|
Code: `portable/capabilities.rs`, `compat.rs`, `extension.rs`, `realm/profile.rs`, `store/profile.rs`, `realm/reconcile.rs`
|
|
(`reevaluate_realm_character`). Not in this phase: Registry, Relay, any network transport (the profile is plain data a Registry can
|
|
publish later), Wildcard, the personal bank, equipment sets, level-cap projection, and an adapter for any real module.
|
|
|
|
## 1. The content profile
|
|
|
|
`RealmCapabilities` is what a destination can take, transport-neutral and small (a few hundred bytes of hashes, no id lists):
|
|
|
|
```text
|
|
RealmCapabilities
|
|
profile_version 1 (a newer one is refused before the shape is read)
|
|
core { commit, branch, date } identity of the realm's core build; for logs and support, not part of the hash; absent when the core was not asked
|
|
content: ContentProfile everything that decides what can be applied
|
|
ruleset coa (Wildcard transfer is disabled)
|
|
character_formats readable 1..2, writable 2 (the Manager side that serves the realm)
|
|
online_import_job_formats what the realm's core reads for an online import (empty: not asked / too old to answer)
|
|
session_protocol 1 (PortableSessionStarted / PortableCheckpoint / OwnerAck)
|
|
collection_protocol 1 (CollectionObserved / CollectionState / CollectionAck)
|
|
features runtime_sessions, wardrobe, collections
|
|
collection_kinds coa:appearance, coa:vanity
|
|
extensions [{namespace, module_version, formats: {min, max}}] sorted by namespace
|
|
client_catalog {Appearances.dbc, ItemAppearances.dbc, VanityCollection.dbc, ItemSet.dbc} -> {sha256, records}
|
|
content_profile_hash SHA-256 of the canonical JSON of `content`
|
|
```
|
|
|
|
* **Hash, not lists.** Two realms with the same `client_catalog` hashes know exactly the same appearances and vanity items. The ids
|
|
themselves are read, where they are needed (the side that writes to the realm), from the realm's own data directory
|
|
(`RealmKnowledge::from_data_dir`), and that read carries the same hashes: an evaluation blocks when the files the Manager read are not
|
|
the ones the profile (and the core) describe.
|
|
* **The hash covers content only.** A new core build with the same content has the same `content_profile_hash`; a different client
|
|
table, a feature switched off, another extension range, another job format or protocol version moves it. A profile from outside is
|
|
size-checked, version-checked, strict (`deny_unknown_fields`) and its hash must be the content's: a forged or edited one is refused.
|
|
* **Sources.** The core (RA `portable capabilities`: commit, branch, date, the job formats it reads, the session marker version, its
|
|
features, and the SHA-256 and record count of the client tables **it loaded**), the realm's schema (which module tables exist), the
|
|
data directory (the catalog), and the registered extension adapters. `probe_capabilities()` assembles them; when the core cannot be asked
|
|
(a stopped realm) the profile says so by the absence of `core`, empty job formats and no `runtime_sessions`: the Manager never guesses a
|
|
capability it did not see. When the core was asked, its catalog must equal the one hashed from the data directory, table by table.
|
|
* **Remembered.** `realm_profile` (schema 7) keeps the last profile of every realm and whether it came `live` or `offline`.
|
|
|
|
## 2. Preflight: explicit outcomes before anything is written
|
|
|
|
`compat::evaluate` returns, for a character and an operation (`OfflineImport`, `OnlineImport`, `Update`, `RuntimeSession`), one `Outcome` per
|
|
topic and an overall `Verdict`:
|
|
|
|
| Outcome | Meaning |
|
|
|---|---|
|
|
| `Compatible` | applied completely |
|
|
| `Held` | partly or wholly **not applied** (ids the realm's client data does not know, an extension in another format); stays in the canonical character, with counts and the reason |
|
|
| `Unsupported` | the realm lacks the feature; nothing applied, nothing lost |
|
|
| `Blocking` | the operation cannot be done on this realm: another ruleset, a character format the realm cannot read, an online import the core cannot take (another job format, or the core was not asked), a session it cannot run, client data that is not what the realm loaded |
|
|
|
|
`Verdict`: `Incompatible` (any blocking), `Degraded` (anything held or unsupported), `Compatible`. The operations that write
|
|
(`import_character*`, `import_character_online[_on]`, `update_realm_character*`) call it **first** when the options carry the profile
|
|
(`ImportOptions::capabilities`) and refuse with `PortableError::Incompatible { operation, reasons }` before the first row, journal entry or job file;
|
|
what is held back is returned in `not_applied` / `warnings`. Without a profile nothing is evaluated (the lower layers' own tests, tools
|
|
that do not know the realm); the harness always assembles one (`--data-dir`, plus `--ra-port` for the core).
|
|
|
|
## 3. Extensions
|
|
|
|
In the canonical character an extension (`mod:<module>`) is an **opaque, hash-protected blob**: the Owner never interprets, edits, drops or applies it.
|
|
`ExtensionAdapter` is the only thing allowed to touch a module's data on a realm:
|
|
|
|
```text
|
|
namespace() mod:<module>
|
|
module_version()
|
|
supported_format_versions() {min, max}
|
|
available(realm) is the module on this realm
|
|
validate(extension) well formed? (also run on everything an adapter exports)
|
|
export(realm) the module's data of this character, or none
|
|
apply(realm, extension) write a validated, compatible payload
|
|
compatibility(extension, profile) default: the format check against what the profile published; an adapter may be stricter
|
|
```
|
|
|
|
`ExtensionRegistry` registers adapters (only `mod:` namespaces, one per namespace, a sane range), lists what a realm supports (`supported_on`),
|
|
evaluates (`evaluate`), exports (`export_all`, validated) and applies (`apply_all`). Rules, each covered by a test with the fake extension:
|
|
|
|
* **A realm without the extension** (no module, or no adapter): the profile does not list the namespace, the outcome is
|
|
`Deferred(MissingOnRealm)`, the destination receives **nothing**, the canonical payload is untouched, and a later realm that has the module
|
|
receives it.
|
|
* **An incompatible format** (older or newer than the realm's range): `Deferred(FormatTooOld | FormatTooNew)`; nothing is written; the payload stays.
|
|
* **A failing or lying adapter** (`apply` fails, a payload fails its own validation or its content hash): `Failed`, nothing is claimed, the next attempt starts again.
|
|
* **Applied once.** What was applied is remembered by content hash (`realm_extension_state`); an unchanged payload is not written again, a changed one is.
|
|
* **The Manager's own blobs** (`coa:*`, e.g. quarantined settings) are canonical-only and never handed to an adapter.
|
|
* **Merge.** A realm contributes only what its own adapter exported (its `B0` and `B1` hold nothing else) and only what changed between them;
|
|
a namespace nobody on that realm understands is not in `B0`/`B1`, so it is not in the delta and survives every session unchanged.
|
|
The Host reads adapter exports into baselines and checkpoints (`LiveBridge::with_extensions`).
|
|
|
|
No adapter for a real module exists; the framework is exercised by a fake module only.
|
|
|
|
## 4. The profile follows the mapping
|
|
|
|
`character_server_mapping.content_profile_hash` (schema 7) is the hash the realm had when the character was last synchronised with it (set
|
|
after every import, update and re-evaluation). When the realm's profile changes:
|
|
|
|
* `HostService::observe_profile` records the new profile; if the hash moved, what the Host sent and received of the **account collections** is forgotten
|
|
so the next look reports every account again and the Owner answers with its canonical sets (the realm may now know ids it did not), and the characters
|
|
whose synchronised hash is not the new one are returned (`mappings_with_other_profile`);
|
|
* `reevaluate_realm_character` looks again at what was held back for such a character **at the same canonical revision**: appearances (selections, outfits)
|
|
the realm can now show and does not have are written, extension payloads that now have an adapter are applied; **only additions**, nothing the realm
|
|
has or changed since is touched; the new hash is recorded. With the same hash it does nothing; with a new hash and nothing new to show it writes nothing.
|
|
|
|
## 5. Verification (Phase 6.1 and 7 gate)
|
|
|
|
| What | Result |
|
|
|---|---|
|
|
| `cargo test --workspace --locked` | see section 6 |
|
|
| Format 1 -> 2 | genuine v1 migrates with an empty wardrobe and its original hash is verified first; v1 with a wardrobe is rejected; wrong hash / unknown field / version 0 / non-numeric refused; stores holding v1 snapshots read them and compare by `semantic_hash` (no spurious revision, the Host accepts the same revision in the new format) |
|
|
| Job format | the core refuses `job_format` 99, absent, non-numeric and 1, with `supported_job_formats`, before parsing the body, nothing written (real worldserver) |
|
|
| Profile | stable, strict, forged hash refused; assembled from a real schema + data directory; the core's report cross-checked with the data directory (a different `Appearances.dbc` is refused); a profile is under 4 KB |
|
|
| Preflight | another ruleset, a reader of format 1 only, a core that reads another job format, no session feature: refused with the reason and **not one row, mapping or journal entry written** (real MySQL, real worldserver); a partly compatible one proceeds and reports what is held |
|
|
| Profile change | simulated: collections reported again and the 200 held-back ids written without a new revision; real MySQL: held-back selection restored at the same revision, the realm's own choice untouched, a second call and a call with nothing new write nothing |
|
|
| Extensions | fake module: absent on realm / incompatible old and new / failing adapter / rubbish payload / unknown namespace / `coa:*`; opaque through sessions and restored on a later realm that has the module |
|
|
|
|
## 6. Limits
|
|
|
|
* The adapter exports are read by the Host's baselines and checkpoints (`LiveBridge`, the simulated realm); the Phase 4 offline `begin_session` / `reconcile_session`
|
|
functions do not take adapters.
|
|
* The online import applies extensions only when it is given the realm's database connection (`import_character_online_on`); `import_character_online` without it
|
|
reports them as not applied.
|
|
* A held-back **selection** is restored by a re-evaluation (a profile change); a held-back **extension** is applied by it; nothing is re-applied when the profile did not change.
|
|
* Whether the profile of a running realm still matches (a core upgraded in place) is noticed only when it is read again; there is no push.
|
|
* The Host driver is still a library plus the harness; the profile is not yet fetched by the Manager application on its own.
|
|
|
|
## 7. Phase 8: profile version 2 (progression)
|
|
|
|
`profile_version` is 2: the profile gains `progression` (the running core's `MaxPlayerLevel`, projection protocol/policy, an opaque `progression_signature`, `scaling_enabled`), outside
|
|
the content hash, and `Feature::LevelProjection`. A stored version 1 profile is migrated on read (content and hash unchanged, `progression = None`); with no progression every operation
|
|
is `Blocking` until the realm is probed through its core. The core report is version 2 (version 1 cores are still read, without progression). See `PORTABLE_LEVEL_PROJECTION.md`.
|