chore(reborn): MR3.1 semantic audit, i18n localization, and fail-closed cleanup

This commit is contained in:
Дмитрий committed 2026-10-22 22:18:28 +05:00
1 parent 351ace1c56
commit a48717340b
12 files changed
+140 -64

No files matched your search

+11 -7
View File
@@ -183,13 +183,17 @@ impl<'a> HostService<'a> {
});
}
if let Some((_, rs)) = self.server_id.split_once(':') {
if let Ok(host_ruleset) = rs.parse::<crate::portable::Ruleset>() {
if offer.ruleset != host_ruleset || model.ruleset != host_ruleset {
return Err(PortableError::RulesetMismatch {
character: model.ruleset,
realm: host_ruleset,
});
}
let host_ruleset = rs.parse::<crate::portable::Ruleset>().map_err(|_| {
PortableError::Invalid(format!(
"malformed or unknown ruleset suffix in server_id: {:?}",
self.server_id
))
})?;
if offer.ruleset != host_ruleset || model.ruleset != host_ruleset {
return Err(PortableError::RulesetMismatch {
character: model.ruleset,
realm: host_ruleset,
});
}
}
self.store
@@ -1561,6 +1561,20 @@ fn reborn_hostile_attacks_are_rejected_with_zero_mutation() {
coll_err2.is_err(),
"Reborn merging wildcard:vanity must fail"
);
// 9. HostService with malformed server_id ruleset suffixes fails closed
for bad_server_id in ["srv-1:unknown", "srv-1:garbage", "srv-1:reborn:extra"] {
let mut h_bad = HostService::new(&mut host, bad_server_id, HostConfig::default());
let mut offer_bad = OwnerService::new(&mut owner)
.offer(id_reb, "srv-1:reborn")
.unwrap();
offer_bad.server_id = bad_server_id.to_string();
let res_bad = h_bad.accept_offer(host_profile, &offer_bad);
assert!(
res_bad.is_err(),
"accept_offer on malformed server id {bad_server_id} must fail closed"
);
}
}
#[test]
+73 -51
View File
@@ -1,62 +1,84 @@
# Warcraft Reborn Portable Characters Audit
# Warcraft Reborn Portable Characters Audit & Semantic State
## 1. Overview & Baseline vs Current Implementation
## 1. Historical Baseline
The released v0.7.0 master branch codebase contained foundational support for local realm execution:
- Mode: `Mode::Reborn`
- Realm ID: `3`
- Databases: characters DB `acore_characters_reborn`, world DB `acore_world_reborn`, shared auth DB `acore_auth`
- Policy: Reborn runs as the active primary realm on the main world port (8085). Simultaneous execution with CoA/Wildcard is strictly unsupported (`"Simultaneous startup supports CoA and Wildcard only."`).
### 1.1 Baseline Discovered Before Implementation
The released v0.7.0 master branch codebase contained foundational support for local realm execution (`Mode::Reborn`, realm ID `3`, characters DB `acore_characters_reborn`, world DB `acore_world_reborn`, shared auth DB `acore_auth`).
Portable character transfers into Warcraft Reborn were temporarily guarded by:
Before the multi-ruleset implementation, portable character transfers into Warcraft Reborn were temporarily blocked by:
```rust
// crates/coa-core/src/portable/realm/mod.rs
Mode::Reborn => Err(PortableError::Invalid("Portable characters are not supported on Warcraft Reborn realms yet.".into())),
```
### 1.2 Implemented in 58a99f0+ and MR1.1
- **RulesetId / Ruleset**: `Ruleset::Reborn` is established across `coa-core`, `coa-registry-proto`, `coa-control-proto`, and TypeScript models (`"reborn"` wire value, display name `"Warcraft Reborn"`).
- **SQLite Portable Storage:** Migration `009_multiruleset.sql` upgrades released databases to permit `reborn` characters in the `character` table CHECK constraint without dropping child records.
- **Collections:** `reborn:appearance` and `reborn:vanity` defined and carried under exact allowlist in `carried_kind_for`. Partitioned under `(profile_id, 'reborn', kind)`.
- **Wire Protocols:** Control protocol v2, Session protocol v3, and Collection protocol v2 bind `ruleset: Ruleset` explicitly in all messages.
## 2. Current MR3 Implementation
In MR1.1, MR2, and MR3, generic multi-ruleset infrastructure was completed and enabled for Reborn:
- **Ruleset Identity**: `Ruleset::Reborn` established across `coa-core`, `coa-registry-proto`, `coa-control-proto`, and frontend TypeScript models (`"reborn"` wire value, display name `"Warcraft Reborn"`).
- **SQLite Storage**: Migration `009_multiruleset.sql` upgrades released databases to permit `reborn` in `character` CHECK constraints without dropping existing character rows or foreign keys.
- **Collections**: `reborn:appearance` and `reborn:vanity` defined and carried under allowlist in `carried_kind_for`. Partitioned under `(profile_id, 'reborn', kind)`.
- **Wire Protocols**: Control Protocol v2, Session Protocol v3, and Collection Protocol v2 bind `ruleset: Ruleset` explicitly in all messages.
- **Fail-Closed Routing & Ingress**: `srv-X:reborn` resolves strictly to the Reborn realm and fails closed if inactive. HostService rejects malformed server-id ruleset suffixes.
- **Automated Verification**: End-to-end claiming, export, snapshot import, runtime session lifecycle, intermediate/final checkpoints, and logout sync pass.
- **Three-Space Isolation**: Torture fixtures prove zero mutation across rulesets:
- Reborn character -> CoA / Wildcard realm (rejected with zero mutation).
- CoA / Wildcard character -> Reborn realm (rejected with zero mutation).
- Reborn collection rejects foreign namespaced entries (`coa:*`, `wildcard:*`).
### 1.3 Implemented in MR3 Vertical Slice
- Concrete support for Warcraft Reborn character import/export, claiming, session lifecycle, checkpoints, and logout sync.
- `ruleset_of(&reborn_db)` returns `Ok(Ruleset::Reborn)`.
- Reborn character class formatting correctly maps vanilla/standard classes (`reborn:class:1..11`) alongside custom classes.
- Direct ingress and routing respect active primary Reborn realm (realm ID 3, `acore_characters_reborn`, `acore_world_reborn`, port 8085) and fail closed when Reborn is inactive.
- Frontend UI displays `RulesetBadge` with human-facing labels ("Warcraft Reborn", "Wildcard", "Conquest of Azeroth"), ruleset filter tabs in Portable Characters screen, and ruleset-filtered Character Picker in the JOIN drawer.
- Three-space isolation fixture and torture tests prove complete mutation isolation:
- Reborn character -> CoA realm (rejected with zero DB mutation).
- Reborn character -> Wildcard realm (rejected with zero DB mutation).
- CoA character -> Reborn realm (rejected with zero DB mutation).
- Wildcard character -> Reborn realm (rejected with zero DB mutation).
- Reborn collections (`reborn:appearance`, `reborn:vanity`) reject foreign namespaced collections.
- Physical external-client smoke is deferred to the combined manual verification gate.
## 3. Actual Reborn Semantic-State Audit
## 2. Ruleset Identity & DB Schemas
- **RulesetId / Ruleset**:
- `Ruleset::Reborn` exists in `crates/coa-core/src/portable/model.rs` and `crates/coa-registry-proto/src/caps.rs`.
- Serialized string: `"reborn"`.
- Display name: `"Warcraft Reborn"`.
- Associated Realm ID: `3`.
- Character Database: `acore_characters_reborn`.
- World Database: `acore_world_reborn`.
- Auth Database: `acore_auth` (shared across all modes).
- **Collections**:
- `reborn:appearance`
- `reborn:vanity`
- Partitioned under `(profile_id, 'reborn', kind)` in canonical SQLite store (`portable.db`).
### 3.1 Gameplay Identity and Class Architecture
Warcraft Reborn in this core is configured via `CoA.ClassModel = "coa wcr"` / `wcr` mode:
- Classes in Warcraft Reborn are standard classic WoW classes (Warrior, Paladin, Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid; class IDs 1..11) rather than CoA custom classes (Barbarian, Witch Doctor, etc., IDs 12..32).
- Reborn spells, class abilities, talent trees, and creature summons use dedicated spell/creature ID ranges and corrections (e.g. `REBORN_CREATURE_OFFSET = 1100000`, spell twin scripts), which operate on standard AzerothCore character persistence structures.
- Reborn characters use the exact same character persistence schema (`acore_characters_reborn`) as vanilla/CoA character databases, cloned directly via `db.clone_structure("acore_characters", &stage_chars, &cache)` in `crates/coa-core/src/realms.rs`.
## 3. Simultaneous Startup & Multiworld Policy
- As verified in `crates/coa-core/src/multiworld.rs` and `crates/coa-core/src/realms.rs`:
- `CoA + Wildcard` simultaneous execution is supported.
- `Reborn` simultaneous execution is explicitly unsupported (`"Simultaneous startup supports CoA and Wildcard only."`).
- This policy is preserved strictly without changes.
### 3.2 Table & State Classification Matrix
## 4. Enabling Reborn in Portable Architecture
To complete the Reborn vertical slice with full strict isolation:
1. Map `Mode::Reborn => Ok(Ruleset::Reborn)` in `crates/coa-core/src/portable/realm/mod.rs:ruleset_of`.
2. Update tests verifying `ruleset_of(&reborn_db).unwrap() == Ruleset::Reborn`.
3. Verify Reborn claiming, export, import, session runtime, checkpoints, and collection isolation.
4. Verify non-negotiable isolation tests:
- Reborn character -> CoA realm (rejected).
- Reborn character -> Wildcard realm (rejected).
- CoA character -> Reborn realm (rejected).
- Wildcard character -> Reborn realm (rejected).
| Category / State | Database Table(s) | Portable Character Mapping | Classification |
| :--- | :--- | :--- | :--- |
| **Identity (name, race, class, gender)** | `characters` | `PortableCharacter.name`, `.race`, `.class`, `.gender` | **Already covered by generic PortableCharacter** |
| **Progression (level, XP, money)** | `characters` | `Progression.level`, `.xp`, `.money` | **Already covered by generic PortableCharacter** |
| **Inventory & Equipment** | `character_inventory`, `item_instance` | `Item` records, container links, slot indices, item entry | **Already covered by generic PortableCharacter** |
| **Enchants, Gems, Random Properties** | `item_instance` (`enchantments`, `randomPropertyId`, `durability`) | `Item.enchantments`, `random_property_id`, `durability` | **Already covered by generic PortableCharacter** |
| **Bags & Bank** | `character_inventory` (bag slots, bank slots) | `Item` container slots; personal bank slots | **Already covered by generic PortableCharacter** |
| **Spells** | `character_spell` | `PortableCharacter.spells` (array of spell IDs) | **Already covered by generic PortableCharacter** |
| **Talents** | `character_talent` | `PortableCharacter.talents` (spec, talent IDs, ranks) | **Already covered by generic PortableCharacter** |
| **Glyphs** | `character_glyphs` | `PortableCharacter.glyphs` (spec, slot, glyph ID) | **Already covered by generic PortableCharacter** |
| **Action Bars** | `character_action` | `PortableCharacter.actions` (button, action, type) | **Already covered by generic PortableCharacter** |
| **Skills & Weapon proficiencies** | `character_skills` | `PortableCharacter.skills` (skill ID, value, max) | **Already covered by generic PortableCharacter** |
| **Quests (Active & Rewarded)** | `character_queststatus`, `character_queststatus_rewarded` | `Quest` records, objective counters, rewarded list | **Already covered by generic PortableCharacter** |
| **Reputation** | `character_reputation` | `Reputation` records (faction ID, standing, flags) | **Already covered by generic PortableCharacter** |
| **Pets & Pet Spells** | `character_pet`, `pet_spell` | `Pet` records, entry, name, spells | **Already covered by generic PortableCharacter** |
| **Character Settings & Keybindings** | `character_settings`, `character_account_data` | `Setting` records (source, data payload) | **Already covered by generic PortableCharacter** |
| **Vanity / Transmog Collections** | `account_appearance_collection`, `account_vanity_collection` | `reborn:appearance`, `reborn:vanity` collections | **Account-wide Reborn collection** |
| **Active Auras / Buffs** | `character_aura` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** (refreshed on login) |
| **Spell Cooldowns** | `character_spell_cooldown` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** |
| **Daily / Weekly Quest Resets** | `character_queststatus_daily`, `weekly`, `monthly` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** |
| **Instance Bindings** | `character_instance`, `character_homebind` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** |
| **Battleground / Arena Ratings** | `character_arena_stats`, `character_battleground_random` | Not persisted in portable snapshot | **Realm-local and intentionally not portable** |
| **Manastorm / Challenge states** | `ascension_manastorm_*`, `coa_character_challenge` | Classified as `Blocking` if active | **Blocking** (blocks export while active challenge exists) |
| **Custom Reborn Tables** | None exist in database | N/A | **Irrelevant** (no custom tables in `acore_characters_reborn`) |
### 3.3 Reborn Extension Requirement Decision
```text
Reborn extension required: NO
```
**Rationale:**
1. Warcraft Reborn builds persistent character state entirely within standard AzerothCore character tables (`characters`, `character_spell`, `character_talent`, `character_action`, `character_skills`, `character_inventory`, `item_instance`, `character_reputation`, `character_pet`).
2. There are no separate Reborn-specific character tables in `acore_characters_reborn` (the schema is identical to `acore_characters`).
3. Reborn class abilities, spells, and talent configurations are encoded natively via standard spell IDs and talent IDs supported by `PortableCharacter.spells` and `PortableCharacter.talents`.
4. Character settings (such as build selections or client UI configs) persist through `character_settings`, which is already captured in `PortableCharacter.settings`.
5. Transmog and cosmetic appearance selections are synchronized cleanly via the `reborn:appearance` and `reborn:vanity` account collections.
6. Therefore, adding a custom typed extension is not needed and would introduce redundant schema without semantic benefit. The generic `PortableCharacter` model provides 100% semantic continuity for Reborn character builds.
## 4. Remaining Manual Verification
- **Combined Physical Real-Client Gate**:
- Live client connection to a running Warcraft Reborn realm.
- Character creation on Reborn, progression, claiming to Manager.
- Transfer/import to a secondary Reborn realm and runtime session verification.
- Verification that CoA and Wildcard realms remain completely isolated.
- Status: **PENDING COMBINED MANUAL GATE**.
+1 -1
View File
@@ -7,7 +7,7 @@ have its saved realm updated immediately.
Accounts are shared with your other local realms. Warcraft Reborn starts with separate characters; switching profiles does not copy existing characters or progress.
Bots are unavailable on Warcraft Reborn. Other optional modules are experimental. Portable character transfers into Warcraft Reborn are not supported. **Portable characters remain experimental.**
Bots are unavailable on Warcraft Reborn. Other optional modules are experimental. Portable Characters support for Warcraft Reborn is implemented experimentally. Automated isolation/import/session tests pass. Physical real-client verification is still pending the combined manual gate. **Portable characters remain experimental.**
Backups and updates include every created realm. Check the realm recorded in a recovery point before restoring it.
+6
View File
@@ -888,6 +888,12 @@ const de: Partial<Record<keyof typeof en, string>> = {
"crash.hideRaw": "Vollständigen Bericht verbergen",
"crash.copyReport": "Bericht kopieren",
"crash.copied": "Kopiert!",
"portable.filter.none": "Keine Charaktere entsprechen dem ausgewählten Spielmodus.",
"portable.filter.all": "Alle",
"browse.hidden_characters_one": "{n} Charakter aus anderen Spielmodi ist ausgeblendet",
"browse.hidden_characters_few": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet",
"browse.hidden_characters_many": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet",
"browse.hidden_characters_other": "{n} Charaktere aus anderen Spielmodi sind ausgeblendet",
};
export default de;
+6
View File
@@ -1029,6 +1029,12 @@ const en = {
"portable.err.not_prepared": "Only a realm added from a file can be removed here.",
"portable.err.launch": "The game could not be started. Check the client in Settings.",
"portable.err.other": "Something went wrong. Nothing was lost.",
"portable.filter.none": "No characters match the selected game mode.",
"portable.filter.all": "All",
"browse.hidden_characters_one": "{n} character from other game modes is hidden",
"browse.hidden_characters_few": "{n} characters from other game modes are hidden",
"browse.hidden_characters_many": "{n} characters from other game modes are hidden",
"browse.hidden_characters_other": "{n} characters from other game modes are hidden",
"portable.diag.title": "Diagnostics",
"portable.diag.text": "A report for a bug report: realms, versions, sessions and recent problems. No passwords and no character data.",
"portable.diag.copy": "Copy diagnostics",
+6
View File
@@ -888,6 +888,12 @@ const es: Partial<Record<keyof typeof en, string>> = {
"crash.hideRaw": "Ocultar informe completo",
"crash.copyReport": "Copiar informe",
"crash.copied": "¡Copiado!",
"portable.filter.none": "Ningún personaje coincide con el modo de juego seleccionado.",
"portable.filter.all": "Todos",
"browse.hidden_characters_one": "{n} personaje de otros modos de juego está oculto",
"browse.hidden_characters_few": "{n} personajes de otros modos de juego están ocultos",
"browse.hidden_characters_many": "{n} personajes de otros modos de juego están ocultos",
"browse.hidden_characters_other": "{n} personajes de otros modos de juego están ocultos",
};
export default es;
+6
View File
@@ -888,6 +888,12 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"crash.hideRaw": "Masquer le rapport complet",
"crash.copyReport": "Copier le rapport",
"crash.copied": "Copié !",
"portable.filter.none": "Aucun personnage ne correspond au mode de jeu sélectionné.",
"portable.filter.all": "Tous",
"browse.hidden_characters_one": "{n} personnage d'autres modes de jeu est masqué",
"browse.hidden_characters_few": "{n} personnages d'autres modes de jeu sont masqués",
"browse.hidden_characters_many": "{n} personnages d'autres modes de jeu sont masqués",
"browse.hidden_characters_other": "{n} personnages d'autres modes de jeu sont masqués",
};
export default fr;
+6
View File
@@ -1011,6 +1011,12 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"portable.err.not_prepared": "Здесь можно убрать только реалм, добавленный из файла.",
"portable.err.launch": "Не удалось запустить игру. Проверьте клиент в настройках.",
"portable.err.other": "Что-то пошло не так. Ничего не потеряно.",
"portable.filter.none": "Нет персонажей для выбранного игрового режима.",
"portable.filter.all": "Все",
"browse.hidden_characters_one": "{n} персонаж из других игровых режимов скрыт",
"browse.hidden_characters_few": "{n} персонажа из других игровых режимов скрыто",
"browse.hidden_characters_many": "{n} персонажей из других игровых режимов скрыто",
"browse.hidden_characters_other": "{n} персонажей из других игровых режимов скрыто",
"portable.diag.title": "Диагностика",
"portable.diag.text": "Отчёт для сообщения об ошибке: реалмы, версии, сессии и недавние проблемы. Без паролей и без данных персонажей.",
"portable.diag.copy": "Скопировать диагностику",
+6
View File
@@ -890,6 +890,12 @@ const zh = {
"crash.hideRaw": "隐藏完整日志",
"crash.copyReport": "复制崩溃报告",
"crash.copied": "已复制!",
"portable.filter.none": "没有符合所选游戏模式的角色。",
"portable.filter.all": "全部",
"browse.hidden_characters_one": "已隐藏 {n} 个其他游戏模式的角色",
"browse.hidden_characters_few": "已隐藏 {n} 个其他游戏模式的角色",
"browse.hidden_characters_many": "已隐藏 {n} 个其他游戏模式的角色",
"browse.hidden_characters_other": "已隐藏 {n} 个其他游戏模式的角色",
} as const;
export default zh;
+3 -3
View File
@@ -68,7 +68,7 @@ function SortHeader({ id, sort, order, onSort, children, className }: { id: Sort
}
function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog: Catalog; onClose: () => void }) {
const { t, locale } = useI18n();
const { t, tn, locale } = useI18n();
const { state, refresh: refreshPortable } = usePortable();
const [detail, setDetail] = useState<RealmDetail | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -159,7 +159,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog:
<p className="text-muted">{t("browse.noCharacter")}</p>
{hiddenCount > 0 && (
<p className="text-xs text-muted">
{hiddenCount} {hiddenCount === 1 ? "character" : "characters"} from other game modes {hiddenCount === 1 ? "is" : "are"} hidden
{tn("browse.hidden_characters", hiddenCount)}
</p>
)}
</div>
@@ -171,7 +171,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog:
</select>
{hiddenCount > 0 && (
<p className="text-xs text-muted">
{hiddenCount} {hiddenCount === 1 ? "character" : "characters"} from other game modes {hiddenCount === 1 ? "is" : "are"} hidden
{tn("browse.hidden_characters", hiddenCount)}
</p>
)}
</div>
+2 -2
View File
@@ -318,7 +318,7 @@ export function PortablePage({ onOpenSettings }: { onOpenSettings?: () => void }
rulesetFilter === "" ? "bg-white/10 text-ink shadow-sm" : "text-muted hover:text-ink"
)}
>
{t("browse.any")}
{t("portable.filter.all")}
</button>
<button
type="button"
@@ -355,7 +355,7 @@ export function PortablePage({ onOpenSettings }: { onOpenSettings?: () => void }
{characters.length === 0 && <Card className="mt-3 p-5 text-sm text-muted">{t("portable.characters.none")}</Card>}
{characters.length > 0 && filteredCharacters.length === 0 && (
<Card className="mt-3 p-5 text-sm text-muted">
No characters match the selected ruleset filter.
{t("portable.filter.none")}
</Card>
)}
<div className="mt-3 space-y-4">