mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-12 20:11:02 +00:00
character_settings policy v2: the carry list is ten strict key patterns (core.ascension_active_spec, _starter, _starter_live, _reset_credits, _slot.active, _slot.#, _slot.#.build.#, _slot.#.bar.#, _build.#, _bar.#), matched exactly, not by prefix. Spell charges, Destiny Weaver, dynamic XP, Runemaster, prestige/glory, tutorial flags, Wildcard state and anything unknown are quarantined in the opaque extension and never applied. The importer's own marker source (coa.portable.) is dropped. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
107 lines
6.7 KiB
Markdown
107 lines
6.7 KiB
Markdown
# Portable export from a realm (Phase 2)
|
|
|
|
Code: `crates/coa-core/src/portable/realm/`. Scope decisions: `PORTABLE_CHARACTERS_AUDIT.md` sections 15-16.
|
|
|
|
Phase 2 is **read-only**. It reads one **offline** character out of a realm's characters database into a
|
|
`PortableCharacter` and (for "Make Portable") registers it in the local store. It never writes to a realm.
|
|
|
|
## What it does
|
|
|
|
| Function | Meaning |
|
|
|---|---|
|
|
| `inspect_characters(db)` | "Inspect realm characters": every character of a non-bot account with the reasons it cannot be exported yet |
|
|
| `export_character(db, guid, character_id, prior_items)` | "Export snapshot": the portable model + item observations + warnings |
|
|
| `make_portable(db, store, profile, server_id, guid)` | "Make Portable": export, then register in the local store in one transaction (revision 1, bound to the realm, items mapped) |
|
|
|
|
No Tauri command or UI exists yet (Phase 9).
|
|
|
|
## How the realm is read
|
|
|
|
* Through the Manager's existing `Db` abstraction (the bundled `mysql` client, or the same client inside a Docker
|
|
database container). No driver, no new connection type, no published port. The Wildcard realm profile is routed by
|
|
`Db` exactly like everything else, and **the database decides the ruleset**, not the caller.
|
|
* **Fixed SQL, typed input.** Every statement is a literal in `script.rs`. The only input is the character's local
|
|
guid as a `u32` formatted as a number. Table and column names of *unknown* tables come from `information_schema`,
|
|
are checked as plain identifiers and appear only in `SELECT COUNT(*)`.
|
|
* **One consistent read-only snapshot.** All statements for a character run in one `mysql` session inside
|
|
`SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;`.
|
|
Verified against a real server: a write inside the script fails, and a concurrent write by someone else in the
|
|
middle of the script is not seen.
|
|
* **Hex-encoded text.** Names, item texts, settings and macro blobs are returned as `x<hex>`, so tabs, newlines, quotes
|
|
and backslashes cannot confuse the parser.
|
|
* Writing into a realm (Phase 3) must use prepared statements or the realm's own in-core import; this read path is not
|
|
a basis for it.
|
|
|
|
## Who may be exported (`Blocker`)
|
|
|
|
| Blocker | Rule |
|
|
|---|---|
|
|
| `Online` | `characters.online <> 0` (also after a crash, when the flag is stale: stop the realm or log in/out) |
|
|
| `Deleted` | `deleteDate` is set |
|
|
| `BotAccount` | account `COABOT*` or `COAMANAGER` |
|
|
| `ActiveChallenge` | a row in `coa_character_challenge` |
|
|
| `ActiveGameMode` | `coa_character_gamemode.gameMode <> 0` |
|
|
| `ActiveCustomTrial` | a row in `coa_custom_trial_active` |
|
|
| `PendingManastormCaches` | a row in `ascension_manastorm_cache` (it names item guids that would be orphaned) |
|
|
| `UnclassifiedState(table)` | a table this Manager does not know has a per-character column with rows for this character |
|
|
|
|
Finished challenges, game mode 0 and condition flags are history, not active state, and do not block (decision D6:
|
|
*active* state blocks). A realm that lacks a core table (`SchemaMismatch`) cannot be exported from.
|
|
|
|
## What is exported (v1)
|
|
|
|
17 tables: `characters`, `character_inventory`, `item_instance`, `character_gifts`, `character_spell`,
|
|
`character_talent`, `character_skills`, `character_glyphs`, `character_reputation`, `character_queststatus`,
|
|
`character_queststatus_rewarded` (`active = 1`, as the realm loads it), `character_action`, `character_pet`, `pet_spell`,
|
|
`character_pet_declinedname`, `character_settings` (by policy), `character_account_data` (macros, type 5).
|
|
|
|
**Not exported by decision** (deferred): collections, the personal Ascension bank, equipment sets, per-character wardrobe
|
|
(`character_appearance*`), `mod_craftsmans_codex`. Auras, cooldowns, mail, guild, social, achievements, daily/weekly
|
|
quests, position and homebind are never part of v1.
|
|
|
|
Left out *with a warning* (the realm would delete them on load anyway): inventory rows without an `item_instance`,
|
|
items in a bag that is not on the character, items in buyback slots.
|
|
|
|
### `character_settings` policy (`policy.rs`, version 2)
|
|
|
|
Only the **minimal build / spec / starter / reset state** is carried, and only by strict key patterns (`#` = 1-9 digits,
|
|
everything else literal; no broad prefixes):
|
|
|
|
```
|
|
core.ascension_active_spec core.ascension_starter core.ascension_starter_live
|
|
core.ascension_reset_credits core.ascension_slot.active core.ascension_slot.#
|
|
core.ascension_slot.#.build.# core.ascension_slot.#.bar.# core.ascension_build.# core.ascension_bar.#
|
|
```
|
|
|
|
* **Quarantine**: everything else (`core.spell_charge.*`, Destiny Weaver, dynamic XP, Runemaster, prestige/glory,
|
|
tutorial flags, Wildcard state, unknown sources) is kept in the snapshot as the opaque extension
|
|
`coa:unlisted-settings` with its values, and is **never applied** at a realm.
|
|
* **Drop**: `coa.bot*`, `coa.gameplay_test`, `coa.highrisk`, `coa.portable.` (the importer's own marker).
|
|
|
|
Version 1 (Phase 2) carried broad prefixes; version 2 is the corrective Phase 2.1 change.
|
|
|
|
### Table registry (`registry.rs`)
|
|
|
|
All 163 tables of the characters schema are classified (`Portable`, `Deferred`, `Collection`, `AccountLocal`,
|
|
`CharacterLocal`, `Blocking`, `Realm`). A test fails if the real schema has a table the registry does not know, and at
|
|
export time an unknown table that holds this character's rows blocks the export. A silent half-export is not possible.
|
|
|
|
## Item identity
|
|
|
|
Realms recycle local item guids. `ItemObservation.identity` hashes what cannot change during an item's life (entry,
|
|
random property, crafter). A re-export keeps an item's portable id only if the identity behind the local guid is
|
|
unchanged; a recycled guid produces a new portable item. The store side is in `PORTABLE_FORMAT.md` (item mapping
|
|
lifecycle).
|
|
|
|
## Tests
|
|
|
|
* `realm/tests.rs`: on **recorded answers of a real MySQL** (`testdata/golden/`), no database needed: naked level 1,
|
|
geared level 80 (bags, enchants, gems, random suffix, crafted-by, texts, charges, gift, currency tokens), quests,
|
|
reputation, action bars, 300 spells + talents + glyphs + skills + CoA settings + binary macros, hunter and summon
|
|
pets with declined names, every blocker, item id stability and recycling, Wildcard ruleset, CRLF, 400 damaged
|
|
answers (no panics), registration in the store (one transaction, refused twice, rolled back on failure).
|
|
* `realm/live.rs` (`#[ignore]`): the same answers re-recorded/verified against a **real disposable MySQL**, plus the
|
|
read-only and snapshot-consistency proofs, the 21 genuine template characters of the schema fixture, and the proof that
|
|
`make_portable` changes nothing in the realm. Instructions are at the top of the file.
|
|
* `tools/gen_portable_fixture.py` writes `testdata/realm-fixture.sql` (deterministic).
|