diff --git a/.gitignore b/.gitignore index 0500ffdef..c71ea85a6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,10 @@ __pycache__/ /mutants.out.old/ .scratch/ +# Research notes stay local. +/docs/research/ +/docs/verification/*-research.md + # Agent-created Git worktrees are local execution state, not repository content. /.claude/worktrees/ diff --git a/docs/research/deeprun-tram-objects.md b/docs/research/deeprun-tram-objects.md deleted file mode 100644 index a1c03586b..000000000 --- a/docs/research/deeprun-tram-objects.md +++ /dev/null @@ -1,96 +0,0 @@ -# Deeprun Tram placed objects - -Research for [issue 648](https://github.com/LyraCoreProject/LyraCore/issues/648), checked against -LyraCore commit `46bf948b27b6d21555832b7505ce06ba5fae4f48` on 2026-10-10. This note records source -inspection. It does not establish the deployed import configuration or a real client's result. - -## Finding and fix scope - -The canonical World Import Profiles omit map 369. Add whole-map coverage to `alliance-eastern`, -`starting-eastern`, and `alliance-single`. These profiles own the Eastern Kingdoms content that -connects to Deeprun Tram. Keep the import script's planned map lists consistent. Existing code can -import the placed objects and deliver their static CREATE packets. No new AOI mechanism is justified -by the evidence. Moving tram cars and client scenery need separate checks. - -The profile catalogue currently covers bounded parts of maps 0 and 1. Only `alliance-single` and -`instances` include a whole map, map 36. The spawn predicate accepts every coordinate on a whole map, -so map 369 should use that path rather than a continent rectangle. The same predicate controls -creature and gameobject coverage. -[Profile catalogue](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/world_import_scope.rs#L160-L198), -[scope predicate](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/world_import_scope.rs#L288-L302), -[gameobject filter](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L4918-L4956). - -## What the external source proves - -The issue's pinned CMaNGOS ClassicDB update adds a Deeprun Chest and replaces 88 Subway Bench spawns -on map 369. It also corrects other placed objects. This proves that some station objects require -gameobject rows. An update file is neither a complete census nor evidence of what an Operator's -assembled dump contains. -[ClassicDB update](https://github.com/cmangos/classic-db/blob/ec4f596146be6467ea93c57397858e329e2db852/Updates/4790_WDB-ironforge_deepruntram_fixes.sql#L35-L141). - -Transport behavior needs more than static placement. CMaNGOS creates a separate elevator transport -for its transport type and sets transport flags, an update flag, pause time, and initial state. -LyraCore's existing CREATE builder always emits a stationary position, zero gameobject flags, and -`has_transport: 0`. Those differences justify a separate tram rendering and movement check. They do -not establish which missing object the play report meant. -[CMaNGOS creation](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/GameObject.cpp#L101-L106), -[transport fields](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/GameObject.cpp#L194-L247), -[LyraCore CREATE](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/gameobject.rs#L58-L93). - -Client terrain and model geometry have a different input path, the Operator's client archives. -The object update does not prove missing archive scenery. Whole-map spawn coverage also adds no -terrain, navigation, or vmap extraction by itself. Those modes iterate explicit bounded slices or -Instance Vmap Slices. Keep geometry changes outside this fix unless archive or client evidence -identifies another defect. -[Ingestion sources](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/data-ingestion.md#L34-L54), -[terrain iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/terrain.rs#L181-L203), -[navigation iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/nav.rs#L710-L734), -[vmap iteration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/vmap.rs#L458-L478). - -## Shard assignment and CREATE delivery - -The documented Shard Map sends map 1 to the Kalimdor World Shard and map 36 to the Instance Pool. -Unmatched maps resolve to the default World Shard, so map 369 belongs with the eastern import under -that topology. The shared dungeon catalogue contains only map 36. This is a conclusion about the -documented topology, not the live Realm's settings. A custom rule for map 369 must have matching -content on its owning Shard. -[Documented rules](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/architecture.md#L330-L340), -[Shard Map resolution](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/config.rs#L203-L211), -[dungeon catalogue](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/crates/lyracore-shared/src/instance.rs#L1-L15). - -The script independently lists planned maps and uses them for spatial ownership checks. Adding 369 -only to the Rust catalogue leaves that contract inconsistent. -[Profile map lists](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/scripts/import-manifest.sh#L38-L45), -[expected maps](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/scripts/import-world.sh#L259-L267). - -The importer retains the spawn's map, position, and quaternion and emits templates for used entries. -The Module loads static gameobjects into instance 0 and computes their AOI cells. The Gateway -subscribes to gameobjects and their templates. Insert, recenter, and resident sweep paths reach the -same Relay. That Relay requires a matching instance and a template on the holding Shard, then emits -CREATE followed by rotation VALUES. A spawn without its template produces no CREATE. -[Import rows and templates](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L4930-L5035), -[Module loading](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/gameobject.rs#L1131-L1175), -[subscriptions](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/connection.rs#L1298-L1299), -[insert dispatch](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_view.rs#L1703-L1731), -[recenter and sweep](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_view.rs#L6141-L6208), -[CREATE Relay](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/subscriptions.rs#L1130-L1177). - -## Verification and remaining limits - -Add a planner test with authored map-369 objects at both station distances. Assert that eastern and -single profiles retain both spawns and templates, preserve map and rotation, and exclude them from -Kalimdor and Instance Pool profiles. Update profile catalogue and shell manifest checks. Existing -tests already cover whole-map spawn preservation, quaternion packing, CREATE serialization, and AOI -partition behavior. No external SQL rows or emulator code need to enter the test fixtures. -[Whole-map and quaternion tests](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L8040-L8133), -[CREATE tests](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/gameobject.rs#L199-L340), -[AOI comparison](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/world_index.rs#L635-L687). - -No local assembled dump appeared in `.import/`. This research ran no import, tests, or live reads. -Offline tests can prove import planning and packet construction. They cannot prove deployed content, -visual rendering, or tram movement. After an authorized reimport, record the source stamp, owning -Shard, map-369 spawn count, and template completeness. A Headless Client can check CREATE delivery. -A real build-5875 client must inspect identifiable placed objects at both entrances and check tram -cars and archive scenery separately. The repository's Verification ladder requires these distinct -checks. -[Verification ladder](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/docs/architecture.md#L706-L724). diff --git a/docs/research/fireball-global-cooldown.md b/docs/research/fireball-global-cooldown.md deleted file mode 100644 index 863cefb6e..000000000 --- a/docs/research/fireball-global-cooldown.md +++ /dev/null @@ -1,87 +0,0 @@ -# Fireball global cooldown timing - -The reference supports starting Fireball's global cooldown when the cast starts. -Launching the projectile must not start another global cooldown. A cast longer -than its global cooldown can therefore be followed by another cast at launch. -This follows from the separate start and completion paths in -[CMaNGOS `Spell.cpp`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L3066-L3084). - -## Reference behavior - -All links pin CMaNGOS Classic commit `8ec338a1704e7dcb1c0213eb7ed58f9231ade40f`. -This is primary evidence of that implementation. It does not prove Blizzard's -original 1.12 internals. - -- [`Spell::Prepare`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L3066-L3084) - calls `AddGCD` before `SendSpellStart`, for ordinary casts with or without a cast - time. -- [`WorldObject::AddGCD` and `HasGCD`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Object.cpp#L2599-L2617) - store the global cooldown category's expiry as the current time plus - `StartRecoveryTime`. The Gate lasts until that expiry. -- [`Spell::cast`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L3271-L3282) - calls `SendSpellCooldown` immediately before the launch message, `SendSpellGo`. - [`SendSpellCooldown`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L3510-L3517) - calls `AddCooldown`, not `AddGCD`. -- [`Player::AddCooldown`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L20196-L20201) - reads `RecoveryTime` and `CategoryRecoveryTime`. Those fields describe the - spell's own cooldown and shared spell category cooldown. They are separate - from the global cooldown fields. - -## Timing to preserve - -Given a 1.5-second global cooldown, a 3-second cast starts the global cooldown at -0 seconds, outlasts it at 1.5 seconds, and launches at 3 seconds. A 1-second cast -launches with 0.5 seconds of the original global cooldown left. These examples -follow the expiry calculation in -[`WorldObject::AddGCD`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Object.cpp#L2599-L2617). -Instant casts also start the global cooldown through `Prepare` before executing. - -The implementation should preserve the global cooldown recorded at cast start -and start any spell cooldown at successful completion. Moving every cooldown to -cast start would change spells with their own cooldowns. - -CMaNGOS also applies global cooldown modifiers and compensates for latency in -[`Player::AddGCD`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L20149-L20182). -Those adjustments do not change which cast phase starts the global cooldown. - -## Cancellation and interruption - -Canceling a timed cast before launch clears its remaining global cooldown in -this reference. [`Spell::cancel`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L3114-L3126) -calls `ResetGCD` in the created, targeting and casting states. -[`ResetGCD`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Object.cpp#L2800-L2811) -erases that global cooldown category. The traveling and channeling cases do not -call it. - -An ordinary client cancel follows -[`HandleCancelCastOpcode`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellHandler.cpp#L394-L409), -[`InterruptNonMeleeSpells`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Unit.cpp#L4384-L4396), -and [`InterruptSpell`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Unit.cpp#L4302-L4328) -to `cancel`. - -Damage pushback keeps the original global cooldown. The -[`damage path`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Unit.cpp#L1270-L1287) -chooses interruption for spells whose damage flags require cancellation, -otherwise it calls -[`Delayed`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L6803-L6840). -That method changes the cast timer without changing the global cooldown. - -A successful -[`interrupt effect`](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellEffects.cpp#L3588-L3608) -also reaches `cancel`, but first applies a school lockout through `LockOutSpells`. -Clearing the global cooldown therefore does not remove that school lockout. - -## LyraCore diagnosis - -The local regression used Fireball with a 2-second cast, a 1.5-second global -cooldown and no separate spell cooldown. Before the fix, the stored global -cooldown expired exactly 1.5 seconds after the completion event. The cast resolver -started it at completion. - -The fix starts the global cooldown when a timed cast is accepted. Completion -rechecks the other Gates and preserves that deadline. Cancellation before launch -releases the global cooldown. The durable tests use an isolated local instance: - -```sh -cargo test -p lyracore-module --test cast_global_cooldown -- --ignored -``` diff --git a/docs/research/hearthstone.md b/docs/research/hearthstone.md deleted file mode 100644 index cac5926e5..000000000 --- a/docs/research/hearthstone.md +++ /dev/null @@ -1,98 +0,0 @@ -# Hearthstone spell identity and cast completion - -Research for [issue 651](https://github.com/LyraCoreProject/LyraCore/issues/651), -against LyraCore commit `0d084310db5dc9d37a60247b5e669ddaf0b71bb8`. - -The item selects the wrong spell. The cast lifecycle already supports the required -delay and cancellation. Fix the seeded item and spell together, preserve the -imported vanilla spell identity, and map the vanilla home-bind teleport effect. - -## Evidence - -- The seeded item 6948 selects spell 50119. Its seeded spell, Call Stone, has one - `E_RECALL_HOME` effect and inherits a zero cast time from `base_spell`. - [Item](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/seed.rs#L496-L514), - [spell](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/seed.rs#L1207-L1219), - [default](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/seed/fixtures.rs#L449-L459). -- The item importer overrides Hearthstone to 50119. Removing that entry preserves - the source item's `spellid_1` and `spelltrigger_1`. - [Override and source-field handling](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/main.rs#L3354-L3370), - [source fields](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/main.rs#L3460-L3470). -- The Gateway sends the template spell ID in the item query. Its codec documents - that the client renders the use text through its own `Spell.dbc`. Sending 8690 - therefore addresses the reported missing use text, but the visible tooltip - still requires a build 5875 client check. - [Item query codec](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/gateway/src/codec/item.rs#L213-L230). -- ClassicDB lists spell 8690 as Hearthstone, with a ten-second cast, raw effect 5 - Teleport Units and a second raw effect 77 Script Effect. Item 6948 has the - return-home use text. These pages corroborate the issue's expected behavior; - they are reference data, not Blizzard's specification. - [Spell](https://classicdb.ch/?spell=8690), [item](https://classicdb.ch/?item=6948). -- The primary CMaNGOS code assigns raw implicit target 9 to - `TARGET_LOCATION_CASTER_HOME_BIND`. It resolves that destination through the - Character's home-bind coordinates and map. Raw effect 5 then applies the - resolved destination, including a different map. - [Target table](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellTargets.cpp#L42), - [destination](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/Spell.cpp#L1840-L1848), - [effect](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellEffects.cpp#L1683-L1731). -- LyraCore's raw effect mapping leaves effect 5 as `E_SCRIPTED`, a no-op. - Its spell header importer already reads cast time from `SpellCastTimes.dbc`. - Preserve that imported time and classify only effect 5 with a home-bind - destination as `E_RECALL_HOME`, with `T_SELF` routing. Mapping every teleport - to home would change other teleport spells. Mapping both Hearthstone effects - to recall would execute the teleport twice. - [Raw mapping](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/spell.rs#L440-L466), - [cast time](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/spell.rs#L1205-L1212), - [target routing](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/spell.rs#L1696-L1721). - -## Existing lifecycle - -`apply_item_use` keeps items whose spell has `E_RECALL_HOME`, then calls -`begin_cast`. A timed spell schedules a `PendingCast` and emits a start event with -the cast duration. `fire_pending_cast` resolves its effects at completion. -`interrupt_cast` deletes the scheduled row, so cancellation prevents recall. -Movement also calls `break_channel`, which cancels the pending cast. No separate -Hearthstone scheduler is needed. -[Item use](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/items/ops.rs#L523-L540), -[cast admission](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/spell/cast/resolve.rs#L1371-L1546), -[completion](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/spell/scheduler.rs#L19-L57), -[cancellation](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/spell/control.rs#L493-L550), -[movement](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/world.rs#L1589-L1595). - -`E_RECALL_HOME` uses the durable Character's bound home and instance 0, through -the shared teleport path. Preserve this destination behavior. -[Recall effect](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/spell/cast/targeting.rs#L1267-L1273), -[recall destination](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/module/src/world.rs#L916-L944). - -## Checks for the fix - -1. Import actual build 5875 data. Item 6948 must retain on-use spell 8690. Its - spell header must have `cast_time_ms = 10000`, with exactly one - `E_RECALL_HOME` effect targeting self. An unrelated raw teleport must remain - unchanged. -2. Decode the item-query response and check the first spell slot is 8690 with - on-use trigger 0. -3. Use a Hearthstone through the durable item-use path. Check start duration - 10000, unchanged location before completion, bound home at completion, - instance 0, and the same item instance and count throughout. -4. Cancel a pending Hearthstone cast and wait past its former deadline. Check - that no completion or teleport occurs and the item remains. Check movement - cancellation through the movement path too. -5. Check the tooltip, cast bar and cancellation in a build 5875 client. A packet - test cannot prove the client renders its local spell description. - -The issue covers spell identity, cast timing and cancellation. Hearthstone's -item cooldown needs separate item-cooldown handling if it is included in scope. -The item query currently defaults its cooldown fields, and the spell importer -only derives cooldowns from spell DBC fields. Do not claim the item cooldown is -fixed by changing the cast time. -[Item query spell fields](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/gateway/src/codec/item.rs#L218-L222), -[spell cooldown import](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/spell.rs#L1213-L1217). - -## Existing Realms - -Publishing does not rerun the initial seed. The post-publish repair does not -replace the Hearthstone item or spell. Re-import both item data and spell 8690 -on every affected Shard to apply this fix to an existing Realm. The old 50119 -spell can remain unused after the item points to 8690. -[Spell import deletion](https://github.com/LyraCoreProject/LyraCore/blob/0d084310db5dc9d37a60247b5e669ddaf0b71bb8/importer/src/spell.rs#L1932-L1958). diff --git a/docs/research/profession-trainer-icons.md b/docs/research/profession-trainer-icons.md deleted file mode 100644 index e8a854070..000000000 --- a/docs/research/profession-trainer-icons.md +++ /dev/null @@ -1,97 +0,0 @@ -# Profession trainer icons - -Investigated issue [649](https://github.com/LyraCoreProject/LyraCore/issues/649) at -LyraCore commit `46bf948b27b6d21555832b7505ce06ba5fae4f48`. No real-client reproduction -or live realm access formed part of this investigation. - -The user identified Fizzlegear in Ironforge and the training window. This points to -Springspindle Fizzlegear, creature entry 5174, whom the -[pinned ClassicDB source](https://github.com/cmangos/classic-db/blob/cd0c426a3b2ff56dd518bf009025299468e60fdb/Full_DB/ClassicDB_1_12_1_z2815.sql.gz) -names "Artisan Engineer". The user has not identified an individual offering. - -## Confirmed packet defect - -The [trainer codec](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/trainer.rs#L31-L67) -always sends trainer list type 0. In build 5875, type 2 enables the created-item -icon lookup for recipe offerings. Type 0 skips that lookup and uses the offering -spell's own icon. This gives a direct cause for incorrect crafted recipe icons. - -The archived [1.12.1 FrameXML](https://github.com/MOUZU/Blizzard-WoW-Interface/blob/d162a4c0d198a4381b5b6573d975635ed7316702/1.12.1/FrameXML/ClassTrainerFrame.lua#L251) -sets the selected service icon through `GetTrainerServiceIcon`. -The [Benilla transcription](https://github.com/Mathih13/benilla/blob/f7666010b182bb0478ee06fbfc6ac3e75e60a2dc/crates/benilla-app/src/ui_trainer/law.rs#L32) -records the native function's rule. I checked its decisive instructions against -the local 1.12.1 executable, without launching it. - -Executable: an unmodified 1.12.1 build 5875 `WoW.exe`. -SHA-256: `b4756d38ef207c02ed651f4952bd89a70b4857b73a33413339e1b285b28d2dc7`. - -```sh -objdump -d --start-address=0x4d8f50 --stop-address=0x4d9150 "$WOW_5875_EXE" -``` - -At `0x4d8fe2`, the function compares the current trainer list type at `0xb73a08` -with 2. Other types branch to `0x4d9008`, which reads the original offering's -`SpellIconID`. Type 2 scans for effect 36 or 57 at `0x4d8ff5` and `0x4d8ffa`, -reads its trigger spell at `0x4d9050`, then reads that spell's product item at -`0x4d906a`. A product item uses its item display icon. A missing product falls -back to the original offering's icon. The same global equals 2 in -`IsTradeskillTrainer` at `0x4d8ea0`. - -## Engineering example from pinned data - -I downloaded the pinned ClassicDB archive into `/tmp/lyra649-classic.sql.gz` -and checked its decompressed SHA-256 against the import lock. It matched -`d2083bcd2670451279cbf93af138eadae04c6d183a4cd0ff0357047e4a565de6`. -This was an offline file read, not a live shard read. - -The source has 52 `npc_trainer` rows for Springspindle Fizzlegear. One offers -spell 3984, "Handful of Copper Bolts", with required skill line 202 and skill -rank 30. Its first spell effect is 36 and triggers recipe 3922. That recipe's -first effect is 24 and creates item 4359, also "Handful of Copper Bolts", with -item display 10700. The offering's own `SpellIconID` is 1. Type 0 chooses that -spell icon; type 2 chooses the product item's display icon. These facts come -from `npc_trainer`, `spell_template`, and `item_template` in the -[same pinned archive](https://github.com/cmangos/classic-db/blob/cd0c426a3b2ff56dd518bf009025299468e60fdb/Full_DB/ClassicDB_1_12_1_z2815.sql.gz). -This is an example for verification, not a claim that the user selected it. - -## Correct type and offering identity - -CMaNGOS [sends each original offering ID and the trainer list type](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/NPCHandler.cpp#L99-L172). -Its [loader sets list type 2](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Globals/ObjectMgr.cpp#L8967-L8968) -when an offering's first trigger teaches a profession. -The [profession test](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellMgr.cpp#L1117-L1131) -checks effect slot 1 for a skill effect and a profession skill line. Other lists -default to 0. This differs from creature template trainer types. -The [skill predicate](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Spells/SpellMgr.h#L1924-L1933) -includes primary professions, Fishing, Cooking, and First Aid. Riding is a -separate case, and combat skill lines are not professions. - -Copying the creature template type would also change weapon master and riding -lists. LyraCore [records weapon masters as template type 2 and riding as 1](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/crates/lyracore-shared/src/trainer.rs#L9-L25). -Use offering facts to derive the packet type. Recognized profession learn rows -provide those facts in the [current importer](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L5090-L5164). -The fix uses those learn rows to identify all twelve imported professions. A custom -list with recipes but no profession learn row retains type 0. Preserve the -original offering ID so the icon lookup and purchase address the same offering. - -## Other observations - -The same reference packet writes talent cost 0 for all offerings and sets -`first_rank` only for the first rank of a primary profession. LyraCore sets both -fields to 1 for every skill learn row. That is a separate purchase-button defect; -it does not control the icon lookup above. - -The [gossip definition](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/GossipDef.h#L64-L75) -assigns the trainer book icon value 3. LyraCore copies imported gossip icons in -[the importer](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/importer/src/main.rs#L2469-L2488), -and [the codec](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/npc.rs#L89-L120) -only adds vendor and innkeeper fallback actions. No trainer gossip defect was -confirmed. Creature NPC flags pass through to the client in -[entity encoding](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/entity.rs#L507). -No cursor defect was confirmed. - -The report identifies an Ironforge engineering trainer and the training window. -The packet defect explains crafted recipe service icons there. The exact -offering the user selected remains unknown. Verify Handful of Copper Bolts at -Springspindle Fizzlegear, then a gathering profession, a secondary profession, -and a class trainer with a real client after the packet fix. diff --git a/docs/research/quest-log-world-entry.md b/docs/research/quest-log-world-entry.md deleted file mode 100644 index ac85d0091..000000000 --- a/docs/research/quest-log-world-entry.md +++ /dev/null @@ -1,116 +0,0 @@ -# Quest log at world entry - -Research for [issue #647](https://github.com/LyraCoreProject/LyraCore/issues/647), checked on -2026-10-10 against LyraCore commit `46bf948b27b6d21555832b7505ce06ba5fae4f48`. -No real-client reproduction or production inspection was performed. - -## Finding - -The strongest code-level explanation is the gap between Character CREATE and the following quest-log -VALUES update. Populate the self CREATE with the existing quest slots so world entry presents the -current log at once. Whether that change removes the reported acceptance feedback still needs a real -1.12.1 build 5875 client check. - -The report does not establish a durable progress reset. It supplies neither the affected map pair -nor the deployed commit. [Issue report](https://github.com/LyraCoreProject/LyraCore/issues/647). - -## Confirmed in LyraCore - -- `MSG_MOVE_WORLDPORT_ACK` routes the Character to its Home Shard and calls `enter_world` with - `WorldEntry::WorldPort`. Both same-Shard map changes and cross-shard Transfer reuse the world-entry - path. [World-port handler](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/world/handlers/char.rs#L468-L524). -- World entry builds a self CREATE without a quest-slot input, then sends the quest slots in a separate - raw VALUES update. That update restores all 20 slots, including zero values for unused slots. - [Entry CREATE](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/world/handlers/char.rs#L165-L186), - [post-CREATE sync](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/world/handlers/char.rs#L282-L294), - [slot mask](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/update_mask.rs#L327-L347). -- The descriptor layout starts at index 198 and has 20 slots of three u32 fields. Each slot contains - a quest ID, packed counters and state, then a timer. The existing codec packs four six-bit counters - in the low 24 bits and state in the high byte. - [Quest field codec](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/update_mask.rs#L298-L345). -- `CharacterQuest` carries counts, reward state, failure state and a deadline. Its Transfer registry - includes the quest rows and re-mints only the surrogate `id`. - [Quest row and Transfer registration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/quest.rs#L393-L466). -- The quest-log descriptor Gate defaults to enabled. An old world-entry comment saying it awaits - verification is stale. - [Configuration](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/config.rs#L566-L575). - -## CMaNGOS comparison - -All CMaNGOS links use the issue's pinned commit `8ec338a1704e7dcb1c0213eb7ed58f9231ade40f`. - -Its explicit quest-accept handler checks the quest giver and quest eligibility before `AddQuest`. -That shows how acceptance occurs, but does not explain the reported client feedback by itself. -[Quest acceptance](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Quests/QuestHandler.cpp#L105-L175). - -`Player::_LoadQuestStatus` restores quest IDs, timers, completion or failure state, and counters into -the Character descriptor. It also clears the unused tail. -[Quest initialization](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L13960-L14048). - -`Object::BuildCreateUpdateBlockForPlayer` writes the descriptor values inside the CREATE block. -`_SetCreateBits` selects populated fields that the recipient can see. This supports carrying the -existing quest state in the initial CREATE. -[CREATE encoding](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Object.cpp#L144-L187), -[recipient fields and CREATE mask](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Object.cpp#L680-L770). -The field table agrees with index 198 and the three-field slot stride. -[Quest descriptor fields](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/UpdateFields.cpp#L140-L179). - -## Client evidence and limits - -An archived copy of Blizzard's 1.12.1 FrameXML listens for `QUEST_LOG_UPDATE`, -`QUEST_WATCH_UPDATE` and `UNIT_QUEST_LOG_CHANGED`. Its quest frame does not handle `QUEST_ACCEPTED`. -The Lua code updates the quest log and watch list through client APIs; it does not show the native -packet-to-feedback rule. -[Archived quest UI](https://github.com/MOUZU/Blizzard-WoW-Interface/blob/d162a4c0d198a4381b5b6573d975635ed7316702/1.12.1/FrameXML/QuestLogFrame.lua#L42-L87). -The same archive contains the `ERR_QUEST_ACCEPTED_S` text used for acceptance feedback. -[Acceptance text](https://github.com/MOUZU/Blizzard-WoW-Interface/blob/d162a4c0d198a4381b5b6573d975635ed7316702/1.12.1/FrameXML/GlobalStrings.lua#L1788). - -The proposed cause is therefore an inference: the client may treat an absent quest ID followed by -a populated ID in VALUES as a newly accepted quest. The inspected source does not prove that native -client behavior. A Headless Client can check packet contents and ordering, but cannot prove that -acceptance text or sound stops. - -## Fix and verification - -Use the existing quest-slot read and mask in self CREATE. Remove the separate initial quest-log -sync. Keep the live quest Relay for acceptance, progress and removal. The existing regression that -expects a post-CREATE update must change to check quest fields inside CREATE. -[Current entry regression](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/world/quest_tests.rs#L90-L122), -[live Relay](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/subscriptions.rs#L3426-L3441). - -Check fresh login, same-Shard map change and cross-shard Transfer through the World Session Seam. -Inspect CREATE quest IDs, counters and state, including empty and multiple-quest logs. Check that no -initial VALUES update introduces the quests after CREATE. Keep the disabled descriptor Gate covered. -For Transfer, compare durable quest rows before and after arrival, excluding re-minted row IDs. - -At a real client, record the map pair, quest IDs and progress. Cross each boundary, return, and -reconnect. Verify that acceptance feedback does not repeat, progress stays intact and a subsequent -objective still updates. Record the exact build and Gateway commit. - -An existing limitation remains outside this packet-order change. `build_quest_log_slots` derives -state only from objective completion and always emits timer zero, so it does not project `failed` -or `deadline_micros`. This research does not claim to fix timed or failed quest display. -[Current slot read](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/stdb/reads/quest.rs#L38-L66). - -## Implementation checks - -The change now puts the quest snapshot in self CREATE. After viewer registration, a writer job -compares the latest quest slots with that snapshot. It sends a full quest VALUES update only when -the slots changed, including when the final quest disappeared. Both this job and live quest Relays -read on the writer, so an earlier snapshot cannot overwrite newer queued progress. - -Local checks on 2026-10-10: - -- The login regression failed before the fix with quest ID 0 in CREATE instead of 777. -- `cargo test -p lyracore-gateway` passed 2,124 tests, with 26 ignored. A final targeted run with - `--bin lyracore-gateway quest` passed 138 tests after adding reconciliation-failure coverage - and populated inventory and skills to the descriptor preservation test. -- `cargo test -p lyracore-module --test quest_transfer -- --ignored --nocapture` passed. Two private - Shards preserved four quests through Escrow, import, finish, release and world-port entry. - Counters, rewarded and failed flags, and deadlines matched; the source quest rows were removed. -- Workspace formatting and Clippy with all targets and features passed. -- Two adversarial reviewers found the registration gap in the first review. Both accepted the - reconciliation fix in the second review. - -These checks do not establish the real client's acceptance-feedback behavior. That remains the -client check described above. diff --git a/docs/verification/watched-reputation-faction-research.md b/docs/verification/watched-reputation-faction-research.md deleted file mode 100644 index 819aebc4c..000000000 --- a/docs/verification/watched-reputation-faction-research.md +++ /dev/null @@ -1,103 +0,0 @@ -# Watched reputation faction research - -Research status: complete. The reporter confirmed that the affected display is the watched bar near -the experience bar. Client Verification of the fix is outstanding. This note checks source at -LyraCore commit `46bf948b27b6d21555832b7505ce06ba5fae4f48`, the commit named in -[issue #650](https://github.com/LyraCoreProject/LyraCore/issues/650). It uses CMaNGOS Classic commit -`8ec338a1704e7dcb1c0213eb7ed58f9231ade40f` and the `wow_world_messages` 0.3.0 source commit -`9a1162cb1696518c4c38c794c18e693f0bd68f51`. No live Realm or client was used. - -## Confirmed protocol behavior - -CMaNGOS initializes `PLAYER_FIELD_WATCHED_FACTION_INDEX` to signed `-1`. It restores the field -from the saved Character value and saves its raw 32-bit representation. Its SQL column is unsigned, -so the saved clear value is `4294967295`. The SQL default of zero is not the Character creation -default. [Creation](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L804), -[restoration](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L14146), -[saving](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/Player.cpp#L15722-L15723), -[SQL column](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/sql/base/characters.sql#L684). - -`CMSG_SET_WATCHED_FACTION` is opcode `0x0318`. The CMaNGOS handler reads one signed 32-bit -value and puts it into that field. It does not change reputation standing and has no range Gate. -[Opcode](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Server/Opcodes.h#L830), -[handler](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/CharacterHandler.cpp#L1119-L1125). - -The field occupies one 32-bit descriptor at absolute index `0x4ED`, or `1261`. CMaNGOS marks it -private to the owning Character. The bundled update-mask setter accepts `i32` at the same index. -[CMaNGOS field](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Entities/UpdateFields.cpp#L315), -[setter](https://github.com/gtker/wow_messages/blob/9a1162cb1696518c4c38c794c18e693f0bd68f51/wow_world_messages/src/helper/vanilla/update_mask/impls.rs#L1527-L1530). - -The selection names a reputation-list slot. `Faction.dbc` has separate fields for the faction ID -and signed `reputationListID`. CMaNGOS keys its reputation state by `reputationListID` and sends -64 initial slots. This supports a safe selection range of `0..=63`, with `-1` for no selection. -Checking that range is a proposed Gate, not behavior copied from the upstream request handler. -[DBC fields](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Server/DBCStructure.h#L359-L385), -[state keys](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Reputation/ReputationMgr.cpp#L243-L262), -[initial slots](https://github.com/cmangos/mangos-classic/blob/8ec338a1704e7dcb1c0213eb7ed58f9231ade40f/src/game/Reputation/ReputationMgr.cpp#L197-L230). - -The bundled generated request decoder disagrees with CMaNGOS. It expects a two-byte `Faction` -enum. Read exactly four little-endian bytes as `i32` for this opcode. -[Generated decoder](https://github.com/gtker/wow_messages/blob/9a1162cb1696518c4c38c794c18e693f0bd68f51/wow_world_messages/src/world/vanilla/cmsg_set_watched_faction.rs#L11-L29). - -## Confirmed omissions and the reported symptom - -At the inspected LyraCore commit, the Character CREATE builder does not set the watched field. -The opcode path checks for a four-byte body, then returns an unavailable notice. The durable -Character row has no watched selection. Login initializes reputation slots in a separate packet. -[CREATE](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/entity.rs#L303-L484), -[request path](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/world/handlers/unavailable.rs#L206-L208), -[Character row](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/character.rs), -[initial factions](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/gateway/src/codec/entity.rs#L730-L757). - -Those omissions establish missing initialization and persistence. The reporter later confirmed -that the Bloodsail Buccaneers display is the watched bar. We have not established that Bloodsail -occupies an omitted field's client default slot, or that standing changed. The original issue -leaves those facts unconfirmed. -[Reported symptom](https://github.com/LyraCoreProject/LyraCore/issues/650). - -## Chosen implementation - -Append `watched_faction_index: i32` to the durable Character row with a default of `-1`. This gives -fresh and preexisting Characters the same no-selection value. Normal Transfers carry the selection -with the whole Character row, which the existing Transfer code serializes and decodes. -[Serialization](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/transfer/transport.rs#L625-L649), -[decoding](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/transfer/mod.rs#L1492-L1500). - -Old in-flight Escrow contains Character bytes without the appended field. Add a narrow decode -compatibility path for that prior row shape, restoring its watched selection as `-1`. Keep malformed -or otherwise unsupported rows as a Refusal. The baseline decoder reads directly into the current -Character type, so a durable column default alone does not handle those old bytes. -[Baseline decoder](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/transfer/mod.rs#L1492-L1500). - -No separate preference table or deletion registration is needed. Character deletion removes the -whole durable row, including the selection. -[Character deletion](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/world.rs#L1243-L1245). - -The Gateway should decode the intent and make a Durable Request. The Module should admit `-1` -and `0..=63`, refuse other values without changing the prior selection, and require current Actor -authority and a live Character entity. The live entity disappears before Transfer exports state. -Avoid adding a taxi restriction accidentally, because the general `actor` helper also refuses -actions during flight. Send the restored selection on every self CREATE, including zero and `-1`. -Use an owner-only field update if the durable selection changes during the World Session. -[Actor authority](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/account_ownership.rs#L481-L529), -[actor helper](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/gw.rs#L152-L160), -[Transfer freeze](https://github.com/LyraCoreProject/LyraCore/blob/46bf948b27b6d21555832b7505ce06ba5fae4f48/module/src/transfer/mod.rs#L1146-L1160). - -## Verification to perform - -Automated checks below exercise protocol and durable state. The graphical client check remains -outstanding. The PR records the completed runs. - -- At the protocol Seam, accept exactly four bytes for `-1`, `0` and `63`. Refuse truncation and - trailing bytes. At the Module boundary, refuse `-2`, `64` and extreme signed values without - changing the selected index or reputation standings. -- Decode self CREATE and owner updates. Assert field `1261` explicitly contains the expected value, - including `FFFFFFFF` for clear and `00000000` for slot zero. Check a fresh Character and a - preexisting Character with no saved selection. -- Select, change and clear a faction. Check each result after login, a same-Shard map change and a - cross-shard Transfer. Check that old Escrow without the appended Character field imports as no - selection, and that malformed rows still cause a Refusal. -- With a real 1.12.1 build-5875 client, check that the unwanted watched bar no longer - appears and that selecting and clearing survives the - lifecycle steps above. Record standing before and after. Keep this symptom outstanding until - that check establishes a connection.