10 KiB
Portable characters in the Manager (Phase 9): Player Mode flow, service, runtime, diagnostics
Integrates the proven engines (store, import/update/reconcile, sessions, level-cap projection) into the application without changing them. Local machine / LAN only: no Registry, Relay or public browsing, no Wildcard, no personal bank or equipment sets. Rust is authoritative; the screen observes state and asks for high-level operations.
1. What the player does
Welcome -> Connect to another server -> Characters. Nothing here needs a local server.
- Realms lists the realms the Manager knows: the servers it installed itself (installed) and realms added from a descriptor file (prepared, section 4).
- Make an existing character portable reads a character that is not in the game from a realm's database and registers it. The Manager keeps a trusted copy (the canonical character) and remembers which realm it came from. The realm's own character is not touched.
- Play on realm... shows the compatibility check first (section 3), then Play. One click prepares everything: the character is put on the realm if it is not there (an update if it is behind), the session is armed, and the screen says Ready. Start the game and log in with this character. Start the game launches the linked client.
- While playing, the Manager checkpoints in the background; on logout it saves the final state. The card shows the resulting saved version and when it was saved.
- Saved versions lists the canonical history in words ("Progress saved while playing on ", "Saved from ").
The player never sees a local GUID, a character or session UUID, a projection manifest, a progression pin, an RA command, a job file, a baseline command or a checkpoint number. Technical detail is available under Technical details of an error and in the diagnostics report.
States
| State | Meaning |
|---|---|
| Ready | The realm's copy is current; nothing is running |
| Preparing character | Play was pressed; the import / update / arm is in progress |
| Waiting for game login | The session is armed; the next login with this character is tracked |
| Playing / Syncing | A session is open; Syncing while checkpoints are in flight |
| Saving progress | The character left the game; the final state is being saved |
| Offline | The realm (or its database) is not reachable |
| Compatibility warning | The realm plays the character with limits (see section 3) |
| Update required | The realm's copy is behind and the realm has to be stopped to be updated |
| Conflict requires action | The realm's copy and the saved character both changed (section 5) |
| Incompatible | The realm cannot take the character; nothing was changed |
2. Layers
React (PortablePage, usePortable) observes PortableState every ~1.2 s, calls portable_* commands
Tauri commands (src-tauri/src/lib.rs) portable_state / preflight / play / make / resolve / history / add_realm / remove_realm / diagnostics / launch
PortableRuntime (service/runtime.rs) worker thread; 1 s tick; installs refreshed every 5 s; publishes a state snapshot; call() runs one action on the thread
PortableService (service/engine.rs) synchronous orchestration over Owner store, Host store, realm access, ServerControl
service/access.rs RealmAccess: how to reach a realm (installed from the registry, prepared from a descriptor)
service/view.rs everything the UI sees; no identifiers of the engines
engines unchanged: store, realm::{import,update,reconcile,project}, session::{Owner,Host,LiveBridge}, projection
- The runtime is started with the application (
start_portable) and stopped onRunEvent::Exit; it is independent of the screen, so navigating away changes nothing. - The Owner and Host stores live under
<data dir>/portable/{owner,host}. A restart resumes sessions, the outbox and the progression pins from them;HostMemorykeeps the Host's in-memory throttling between ticks. After a restart the Host has no memory of its last checkpoint and checkpoints at once; that checkpoint makes a canonical revision only if the character really differs (section 8). - One operation at a time (
busy); status derivation is a pure function of stores + observations (copy_status). COA_MANAGER_DATA_DIRoverrides the data folder (development and tests only).
3. Compatibility before anything is written
preflight(character, realm) reads the realm (capabilities, cap, running state) and evaluates the Phase 7 report for the operations that Play would do. It writes nothing.
| Verdict | Play | Shown |
|---|---|---|
| Compatible | yes | "The character is already on this realm and up to date" / "The Manager will put the character on this realm" |
| Degraded | yes | what stays safely in the canonical character; for a level cap: "This realm has level cap 60. Your level-80 character will play here as level 60. Your full level-80 progression is preserved." |
| Incompatible | no, refused before any mutation | the reasons |
The next step is Prepare (not on the realm), Arm (on the realm and current), Resume (a session is already armed or open), Restart (behind, or the realm's progression
changed: the realm has to be stopped), Resolve (conflict), Offline, Blocked. The projection itself is decided by the core and applied by the Manager exactly as in Phase 8;
there is no manual projection step. After make portable the Manager records the realm's native progression pin for the copy, so the realm's own character is current
and Play needs no restart.
4. Realms
- Installed (a server this Manager runs): derived from the registry/repack settings; id
srv-<install id>; the Manager can stop and start it, so an update of the realm's copy is done automatically (ServerControl: stop, database up, update with the next session armed in the same transaction, start). - Prepared (somebody else's / a development realm): a JSON descriptor, schema 1,
deny_unknown_fields, in<data dir>/portable/realms/<id>.json(added with Add realm file...).
{ "schema": 1, "id": "friends-realm", "name": "Friends' realm", "address": "192.168.1.20",
"mysql_bin": "C:/path/to/mysql/bin", "db_port": 3306, "db_user": "...", "db_password": "...",
"ra_port": 3443, "ra_user": "...", "ra_password": "...",
"job_dir": "C:/path/to/realm/job-dir", "data_dir": "C:/path/to/realm/Data", "game_server_users": ["acore"] }
The descriptor holds credentials; it is trusted-local (same machine / LAN) material, never printed (Redacted), never part of diagnostics. An update of a prepared realm's copy is
not automated (the Manager cannot stop it): Play says Update required and the realm's owner restarts it.
5. Conflicts
If the realm's copy changed outside the Manager while the saved character also moved, preview_update reports the conflicting fields and the card shows Conflict requires action.
There is no automatic merge. Allowed answers: Use the saved character (installed realms; the realm's copy is overwritten with the canonical one, in a stopped-realm update),
Detach the realm copy (the Manager stops managing it; the realm's character is left exactly as it is), Cancel.
6. Diagnostics
Copy diagnostics produces a redacted JSON report: Manager version, per realm: id, kind, online, capability profile hash, core commit, level cap, progression signature, session protocol; per character: id, canonical revision; per copy: realm, synced revision, session id and state, last checkpoint, progression pin, projected flag, compatibility result; the recent errors. No credentials, no snapshots, no inventory.
7. Verification
- Unit tests: descriptor round trip / refusals / secrets never printed, installed realm derivation, history notes never expose identifiers.
- Live (
#[ignore]d, real worldserver, real disposable databases, a bot standing in for the game client):service::live_service::one_click_play_of_a_level_eighty_on_a_cap_sixty_realm_survives_navigation_a_restart_and_the_logout(offline refusal, preflight, one-click Play with projection 80 -> 60, Host restart mid-session, final sync, history, diagnostics redaction, incompatible realm blocked with nothing written) anda_character_made_portable_on_a_running_realm_is_armed_on_it_without_a_restart. - The Tauri window was driven over WebView2's DevTools protocol on a fresh data folder: add descriptors, make portable, preflight with the projection warning, Play, status chips, navigation away and back, a Manager restart in the middle of a session, logout and the saved version, history wording.
- Not covered by automation: the real game client (needs the owner's run, see the report).
8. Checkpoints after a restart (Phase 9.1)
The immediate checkpoint after a Manager/Host restart stays: it is the safety net for whatever happened while nobody watched. The Owner makes a canonical revision from a
checkpoint only when merge3(C0, B0, B1) differs from the head by content_hash; a checkpoint that reproduces the head is acknowledged (Applied) and the revision stays.
Investigated on a real worldserver (cap 60, bot AI suspended with botcmd suspend so that nothing but the realm acts) and on the owner's real-client store: the restart itself
adds nothing. The revisions seen "for no reason" were the first checkpoint of a first login on a realm, which carries what the realm itself did to the imported character
after B0 (default build slot core.ascension_slot.0, flight paths, class/mount spells such as 6654/23250/32243/32246, explored areas) and, with a bot standing in for the
player, the bot's own activity (exploring, an auto-accepted quest). Those are real character state, so they belong in the canonical character; a second login on the same realm
shows none of them. Logging out with nothing changed also makes no revision.
Regression tests: session::tests::the_immediate_checkpoint_after_a_host_restart_makes_no_revision_when_the_character_did_not_change (fake realm, file-backed Host: checkpoint N,
restart, immediate checkpoint Applied, revision unchanged field by field and by hash, then a real change makes exactly N+1) and the live
service::live_service::a_manager_restart_with_nothing_to_report_makes_no_revision_and_a_real_change_makes_exactly_one (real worldserver, PortableService reopened mid-session,
botcmd professiontrainer as the gameplay change).