diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 1bd005e1..129c04a3 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -75,6 +75,27 @@ CPU, so it silences already-playing voices instantly without touching the retail mixing math or persisted volumes. Once the trigger works, no further audio-side work is needed. +## #359 — 0x019E PlayerKilled line prints to participants — retail suppresses it + +**Status:** OPEN — filed 2026-08-09 at the CH1 Opus review. Pre-existing (not +introduced by CH1); candidate for CH4/CH5. + +**Symptom:** `ChatLog.OnPlayerKilled` (`src/AcDream.Core/Chat/ChatLog.cs`) +always appends the death message for every recipient of the `0x019E` +PlayerKilled GameEvent. Retail's `ClientCombatSystem::HandlePlayerDeathEvent +@0x0056C320` skips the `AddTextToScroll` call when the receiving player IS a +participant — `player_id == victim || player_id == killer` — so the victim +and killer see the notification through their own dedicated +Victim/KillerNotification lines (0x01AC/0x01AD) instead, and would see it +twice if the bystander-facing PlayerKilled line were not suppressed for +them. acdream has no such guard: `OnPlayerKilled` prints unconditionally +regardless of whether the local player is the victim, the killer, or an +uninvolved bystander. + +**Fix shape:** thread the local player's guid into `OnPlayerKilled` (or its +caller) and skip the append when it matches `victimGuid` or `killerGuid`, +matching retail's participant check. + ## #357 — Login stalls: reveal reaches ready=True but the player is never placed; UI + sky render, world never opens **Status:** CLOSED 2026-08-08 — root-caused and fixed same session (see the diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 5afc5a77..67e24ad2 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -170,7 +170,7 @@ readiness/requeue adaptation. See --- -## 3. Documented approximation (AP) — 124 active rows (AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) +## 3. Documented approximation (AP) — 125 active rows (AP-176 filed 2026-08-09 at the CH1 Opus review — `ChatLog.OnWeenieError` and `OnCombatLine` type chat lines with a single stand-in `LogTextType` (`0x00`/`0x06`) instead of retail's per-code/per-message dispatch; AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84 collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered @@ -233,6 +233,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-39 | Chat lines carry one solid color per line (retail's exact 34-value `LogTextType` table as of Campaign CH slice CH1, 2026-08-09 — see `RetailChatColorTable`, no longer the earlier synthetic per-`ChatKind` approximation); retail `UIElement_Text` supports per-glyph styled runs (bold, different hue per segment) | `src/AcDream.UI.Abstractions/Panels/Chat/RetailChatColorTable.cs`; consumers `src/AcDream.App/UI/Layout/ChatWindowController.cs`, `src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs` | Retail glyph-run parsing lives inside keystone.dll with no PDB/decomp; per-line coloring is now the exact retail tonal palette (`ChatInterface::BuildChatColorLookupTable @0x004F31C0`), not an approximation of it | Chat lines retail renders with multiple colors or bold names (e.g. "PlayerName says: text") render as one flat color; subtle visual difference but functionally complete | `UIElement_Text` glyph-run styling (keystone.dll, no decomp); `docs/research/2026-08-09-chat-retail-color-table.md` | | AP-40 | Chat uses one fixed `0.75` outer opacity and has no descendant-focus-driven active/default opacity transition | `src/AcDream.App/Rendering/GameWindow.cs` chat mount; `ChatWindowController.cs` | Font resolution is now live and per-element; only the opacity behavior remains deferred to the shared window/focus runtime | Focused chat remains too translucent and idle chat never restores the configured default alpha | `ChatInterface::SetOpacity @ 0x004F3120`; `SetDefaultOpacity @ 0x004F3BC0`; `SetActiveOpacity @ 0x004F3C40` | | AP-175 | PopUpString (`GameEvent 0x0004`) renders as an ordinary chat-log line (`ChatKind.Popup`) instead of retail's MODAL DIALOG. Filed 2026-08-09, Campaign CH slice CH1 (color table) — the color-table work routes this entry through the new 34-value `LogTextType` table (fixed at `0x00` Default/green, unchanged from the entry's pre-existing color) but does not change WHERE it renders; a modal-dialog port is out of this slice's scope | `src/AcDream.Core/Chat/ChatLog.cs` (`OnPopup`); `src/AcDream.Core.Net/GameEventWiring.cs:126` | Informational popup text still reaches the player via the chat transcript; a full modal-dialog port is deferred work, not a color-table concern | Any retail-specific PopUpString behavior contingent on being a blocking modal (e.g. must-acknowledge) is not reproduced; acdream's chat-log line can be missed or scrolled past instead | `ClientCommunicationSystem::Handle_Communication__PopUpString @0x0057FE80`; `docs/research/2026-08-09-chat-retail-color-table.md` §5.1 | +| AP-176 | `ChatLog.OnWeenieError` types EVERY WeenieError/WeenieErrorWithString chat line `0x00` Default; retail's `HandleFailureEvent` dispatches per ERROR CODE across an ~87-case switch, mostly `AddTextToScroll(..., 0, ...)` but a scattered handful at `0x1a` (client-local red). The same class of approximation covers `ChatLog.OnCombatLine`'s generic `0x06` Combat fallback, used by callers (kill/death notifications aside) with no more specific hit/miss/evade classification in hand — a single stand-in type where retail's per-message dispatch would pick one of several. Filed 2026-08-09 at the CH1 Opus review | `src/AcDream.Core/Chat/ChatLog.cs:195-223` (`OnWeenieError`); `src/AcDream.Core/Chat/ChatLog.cs:366-379` (`OnCombatLine`) | `0x00`/`0x06` match each switch's majority behavior and are safe baselines; a full per-code/per-message port is out of CH1's color-table scope | Wrong chat color for the WeenieError codes and combat-line kinds retail types distinctly; pre-SpewBox routing also means even a correctly-`0x1a`-typed WeenieError still renders in the transcript rather than retail's separate on-screen text | `ClientCommunicationSystem::HandleFailureEvent @0x00571990`; `docs/research/2026-08-09-chat-retail-interface-text.md` Appendix A; retired by CH2's full `HandleFailureEvent` table port | | AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (`0x06004C63`) used only when `ThumbTopSprite`/`ThumbBotSprite` are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | `src/AcDream.App/UI/UiScrollbar.cs:35` | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | `UIElement_Scrollbar::UpdateLayout @0x4710d0`; cap sprites `0x06004C60` (top) + `0x06004C66` (bottom) from base layout `0x2100003E` | | AP-42 | `UiMenu` item model is flat (label + opaque payload, single-level popup); retail `UIElement_Menu::MakePopup @0x46d310` supports hierarchical nested submenus via recursive popup chain | `src/AcDream.App/UI/UiMenu.cs` | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | `UIElement_Menu::MakePopup` @0x46d310 | | AP-45 | `PublicUpdatePropertyInt (0x02CE)` sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | `src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs` | Loopback ACE rarely reorders; this property stream has not yet joined the per-object freshness owner introduced for physics messages. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | `PublicUpdatePropertyInt` sequence byte (ACE GameMessagePublicUpdatePropertyInt) | diff --git a/docs/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index 1917a469..fce25b6b 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -99,7 +99,7 @@ implementer per slice against a pinned contract (per | Slice | Commit | Suite | Review | User gate | |---|---|---|---|---| | R1–R4 research | `see docs/research/2026-08-09-chat-retail-*` | — | — | — | -| CH1 colors | `172c6f9a` | 11,833 passed / 4 skipped / 0 failed | in review | pending | +| CH1 colors | `172c6f9a` | 11,835 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed `5f85ec9c` | pending | | CH2 interface text | — | — | — | — | | CH3 side channels | — | — | — | — | | CH4 commands | — | — | — | — | diff --git a/docs/research/2026-08-09-chat-retail-color-table.md b/docs/research/2026-08-09-chat-retail-color-table.md index ebf4e080..d5aa8673 100644 --- a/docs/research/2026-08-09-chat-retail-color-table.md +++ b/docs/research/2026-08-09-chat-retail-color-table.md @@ -267,15 +267,26 @@ Anchors are the `ClientCommunicationSystem::Handle_Communication__*` handlers. `Handle_Communication__ChannelBroadcast` channel-bit → type (the `m_buffer_5` variable feeding `AddTextToScroll` at `0x00571169`): +**Corrected 2026-08-09 (review):** the row 8 rows below marked "corrected" +were wrong in the original CH1 drop. Binary Ninja renders retail's `neg esi; +sbb esi, esi` idiom — a branchless select between `Channel` (0x08) and +`Channel_Send` (0x09) — as the trivial pseudo-C expression `esi - esi` +(always 0), which hid the real values. The correction comes from decoding +the raw bytes at the PDB-paired binary: the HEAR branch's `sbb` site is at +VA `0x00570F0A` (mask `-6` → `0x08` Channel) and the SEND branch's is at VA +`0x00570D4F` (mask `-5` → `0x09` Channel_Send). + | Channel bit | Prefix retail prints | Type | |---|---|---| +| `0x0001` Abuse | `[]` | `0x0E` — retail's ONLY 0x0E producer (corrected 2026-08-09) | +| `0x0400` Help | `[]` | `0x0F` | | `0x0800` Fellowship | `[Fellowship]` | `0x13` | -| `0x1000` Patron / `0x2000` Vassal | `Your patron …` / `Your vassal …` | `0x0A` | +| `0x1000` Patron / `0x2000` Vassal | `Your patron …` / `Your vassal …` | `0x0A` (hear) / `0x0B` (own send) — own-send precision added 2026-08-09 (corrected) | | `0x4000` Follower/Monarch | `Your follower …` | `0x0A` (hear) / `0x0B` (own send) | | `0x1000000` Co-Vassals | `[Co-Vassals]` | `0x0A` | | `0x2000000` Allegiance Broadcast | `[Allegiance Broadcast]` | `0x0A` | -| `0x4000000` | — | `0x13` | -| admin/audit/sentinel channels | `[]` | `0x0E` / `0x0F` | +| `0x4000000` FellowBroadcast | — | `0x08` (hear) / `0x13` (own send) — corrected 2026-08-09, was wrongly `0x13` for both | +| admin/audit/advocate/QA/sentinel/town catch-all | `[]` | `0x08` (hear) / `0x09` (own send) — corrected 2026-08-09, was wrongly `0x0E` / `0x0F` for all of them | Note the split that surprises people: **legacy allegiance-family chat arrives as `Social` (`0x0A`, yellow) / `Social_Send` (`0x0B`, dark yellow)**, while diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index 79d60dda..b8794f74 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -345,7 +345,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory d.Actions.Selection.SelectedObjectId ?? 0u, stackSplitQuantity: d.StackSplitQuantity, systemMessage: - text => d.Communication.Chat.OnSystemMessage(text, 0x1Au), + text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal), sendPutItemInContainer: (item, container, placement) => session.CurrentSession?.SendPutItemInContainer( item, @@ -415,7 +415,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory target, spellId), displayMessage: - text => d.Communication.Chat.OnSystemMessage(text, 0x1Au), + text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal), incrementBusy: itemInteraction.IncrementBusyCount, canSend: () => late.Session.IsInWorld); @@ -753,7 +753,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory iconComposer.GetIcon, itemInteraction, d.Actions.Selection, - text => d.Communication.Chat.OnSystemMessage(text, 0x1Au)), + text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)), Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager), Confirmations: new ConfirmationRuntimeBindings( (type, context, accepted) => @@ -768,7 +768,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory item, inscription), text => - d.Communication.Chat.OnSystemMessage(text, 0x1Au)), + d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)), StackSplitQuantity: d.StackSplitQuantity, Plugins: d.UiRegistry, Persistence: persistence, diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 69a4505b..7d815a0a 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -1125,7 +1125,7 @@ internal sealed class SessionPlayerCompositionPhase d.Actions.CombatMode, d.Log, debugToast, - text => d.Communication.Chat.OnSystemMessage(text, 0x1Au)); + text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)); bindings.Adopt( "live combat-mode commands", d.CombatModeCommands.BindOwned(combatCommand)); diff --git a/src/AcDream.App/Net/LiveSessionCommandRouter.cs b/src/AcDream.App/Net/LiveSessionCommandRouter.cs index b7b02c52..f4c077f5 100644 --- a/src/AcDream.App/Net/LiveSessionCommandRouter.cs +++ b/src/AcDream.App/Net/LiveSessionCommandRouter.cs @@ -240,11 +240,11 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting bindings.Chat.OnSelfSent( ChatKind.Tell, command.Text, - targetOrChannel: command.TargetName, // Retail's own "You tell ..." echo is Speech_Direct_Send // (0x04), distinct from an incoming Tell's 0x03 — see // ChatMessageType.OutgoingTell's "You tell ..." comment. - logTextType: 0x04u); + logTextType: (uint)RetailLogTextType.SpeechDirectSend, + targetOrChannel: command.TargetName); return; } @@ -290,7 +290,9 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting targetOrChannel: legacy.Value.DisplayName, // Precise per-bit own-send type (LegacyChannelChatType.Resolve's // ownSend:true branch) — e.g. Fellowship keeps 0x13, Patron/ - // Vassal/Follower become 0x0B, the admin catch-all stays 0x0E. + // Vassal/Follower become 0x0B, the admin/audit/sentinel + // catch-all becomes 0x09 Channel_Send (corrected 2026-08-09, + // Opus review of 172c6f9a — was wrongly 0x0E). logTextType: LegacyChannelChatType.Resolve(legacy.Value.ChannelId, ownSend: true)); } diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs index 12a76231..eef1696d 100644 --- a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -327,10 +327,15 @@ internal sealed class LiveSessionRuntimeFactory ToggleUiLock: () => _interaction.Settings.SetUiLocked( !_interaction.Settings.Gameplay.LockUI), - // Client-local text (never reaches the wire), 0x1A — same - // category as ChatVM.ShowSystemMessage. + // ClientCommandController's informational output sink — 0x00 + // Default, NOT 0x1A (corrected 2026-08-09, Opus review of + // 172c6f9a). Retail types command output like @version/@loc + // green; 0x1A (bright red) is reserved for genuine refusals. + // The refusal-vs-informational split lands with CH2's producer + // rewiring (SpewBox routing) — see + // docs/research/2026-08-09-chat-retail-interface-text.md §7.2. ShowSystemMessage: - text => _domain.Communication.Chat.OnSystemMessage(text, 0x1Au), + text => _domain.Communication.Chat.OnSystemMessage(text, 0x00u), ShowWeenieError: code => _domain.Communication.Chat.OnWeenieError(code, null), PlayerPublicWeenieBitfield: () => diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs index 7c7625ba..c3da0473 100644 --- a/src/AcDream.App/UI/Layout/ChatWindowController.cs +++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs @@ -461,7 +461,14 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta // appended line — an out-of-range LogTextType leaves it unchanged // rather than reverting to a default (research doc §3.2). Seed the // carry with retail's own unfilled-slot default (colorGreen, index - // 0x00) and fold forward across the transcript in order. + // 0x00) and fold forward across the transcript in order. This is an + // approximation of retail's LayoutDesc-initialized m_curFontColor: + // acdream re-seeds at 0x00 and restarts the fold every render + // window rather than carrying one persistent field across the + // window's whole lifetime. Unreachable in practice today — no + // producer emits a LogTextType >= 0x22 (RetailChatColorTable.Colors + // covers the full 0x00-0x21 retail index space), so the carry path + // below never actually fires outside tests. RetailChatColorTable.TryGetColor(0x00u, out Vector4 currentColor); var result = new List(detailed.Count); foreach (var d in detailed) diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index 11b6f632..48b9b44d 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -263,7 +263,7 @@ public static class GameEventWiring // ClientCombatSystem::HandleKillerNotificationEvent @0x0056C410 // (pc:359548-359559), which calls AddTextToScroll(..., 0, 1, 0) // — LogTextType 0x00 Default, not a combat color. - if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, CombatLineKind.Error, logTextType: 0x00u); + if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, logTextType: 0x00u, kind: CombatLineKind.Error); }); registrar.Register(GameEventType.DefenderNotification, e => { @@ -302,7 +302,7 @@ public static class GameEventWiring { var p = GameEvents.ParseKillerNotification(e.Payload.Span); // Same handler/type as VictimNotification above — 0x00 Default. - if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, CombatLineKind.Info, logTextType: 0x00u); + if (p is not null) chat.OnCombatLine(p.Value.DeathMessage, logTextType: 0x00u, kind: CombatLineKind.Info); }); // ── Spells ──────────────────────────────────────────────── diff --git a/src/AcDream.Core/Chat/ChatLog.cs b/src/AcDream.Core/Chat/ChatLog.cs index 2623ae6b..0d09a3fb 100644 --- a/src/AcDream.Core/Chat/ChatLog.cs +++ b/src/AcDream.Core/Chat/ChatLog.cs @@ -98,10 +98,11 @@ public sealed class ChatLog /// (speech.ChatType) — passed through VERBATIM, with zero /// remapping, matching retail's Handle_Communication__HearSpeech /// @0x005712A0 (the raw arg5 feeds AddTextToScroll - /// directly). Defaults to 0x02 (Speech) for callers that + /// directly). Required — no production caller relies on a default; + /// retail's normal value is 0x02 (Speech) for callers that /// don't have a wire value in hand. /// - public void OnLocalSpeech(string sender, string text, uint senderGuid, bool isRanged, uint logTextType = 0x02u) + public void OnLocalSpeech(string sender, string text, uint senderGuid, bool isRanged, uint logTextType) { // Phase J: ACE's HandleActionTalk broadcasts a HearSpeech echo // back to the sender too. Detect own echo by guid match and @@ -134,7 +135,7 @@ public sealed class ChatLog // Retail hard-codes Emote (0x0C) for every HearEmote line — // ClientCommunicationSystem::HearEmote @0x0057CBE0, the // literal constant at 0x0057CF94. Not a wire value. - LogTextType = 0x0Cu, + LogTextType = (uint)RetailLogTextType.Emote, }); } @@ -150,7 +151,7 @@ public sealed class ChatLog { // HearSoulEmote tail-calls HearEmote @0x0057D096 — same // hard-coded 0x0C. - LogTextType = 0x0Cu, + LogTextType = (uint)RetailLogTextType.Emote, }); } @@ -171,14 +172,13 @@ public sealed class ChatLog SenderGuid: victimGuid, ChannelId: killerGuid) { - // Inferred by analogy from the sibling GameEvents this opcode - // shares a dispatch pattern with: VictimNotification (0x01AC) - // and KillerNotification (0x01AD) both route through the SAME - // retail handler, ClientCombatSystem::HandleKillerNotification - // Event @0x0056C410 (cases 0xa/0xb of the combat-envelope - // switch, pc:359548-359559), which calls - // AddTextToScroll(..., 0, 1, 0) — type 0x00 Default. No direct - // decomp citation was traced for 0x019E PlayerKilled itself. + // Direct anchor (corrected 2026-08-09, Opus review of + // 172c6f9a — replaces the earlier sibling-opcode analogy): + // opcode 0x019E dispatches via UIQueueManager::ProcessNet + // BlobData's byte table @0x55CB07 -> case 7 -> + // ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320 -> + // AddTextToScroll(..., 0, 1, 0) @0x0056C3D8. Type 0x00 Default + // confirmed. LogTextType = 0x00u, }); } @@ -216,8 +216,9 @@ public sealed class ChatLog // Retail's HandleFailureEvent @0x00571990 dispatches per ERROR // CODE across an ~87-case switch, mostly AddTextToScroll(..., // 0, ...) with a scattered handful at 0x1a (client-local red). - // A full per-code port is future work; 0x00 (Default) matches - // the switch's majority behavior and is the safe baseline. + // A full per-code port is future work (register row AP-176); + // 0x00 (Default) matches the switch's majority behavior and is + // the safe baseline. LogTextType = 0x00u, }); } @@ -262,11 +263,11 @@ public sealed class ChatLog /// GameEvent Tell (0x02BD) — whisper received. /// /// The wire chatType from the Tell GameEvent payload - /// (GameEvents.Tell.ChatType) — retail's normal value is - /// 0x03 (Tell), which is also this parameter's default for - /// callers without a wire value in hand. + /// (GameEvents.Tell.ChatType) — required; no production caller + /// relies on a default. Retail's normal value is 0x03 (Tell) + /// for callers without a wire value in hand. /// - public void OnTellReceived(string sender, string text, uint senderGuid, uint logTextType = 0x03u) + public void OnTellReceived(string sender, string text, uint senderGuid, uint logTextType) { Append(new ChatEntry( Kind: ChatKind.Tell, @@ -352,19 +353,26 @@ public sealed class ChatLog /// warning().combat() / error().combat() tag flow at /// chat.rs:221-308. /// + /// The pre-formatted combat line text. /// - /// The retail LogTextType for this combat line. Callers should - /// pass one of the ACE-cited combat types (0x16 Combat_Self for - /// lines about the local player's OWN offensive action, 0x15 - /// Combat_Enemy for lines about an enemy's action against the local - /// player — see ) or 0x00 - /// Default for retail's decompiled kill/death-notification color - /// (HandleKillerNotificationEvent @0x0056C410). Defaults to - /// 0x06 (the generic Combat slot) for callers with no more - /// specific classification in hand. + /// The retail LogTextType for this combat line — required; no + /// production caller relies on a default. Callers should pass one of + /// the ACE-cited combat types (0x16 Combat_Self for lines about + /// the local player's OWN offensive action, 0x15 Combat_Enemy + /// for lines about an enemy's action against the local player — see + /// ), 0x00 Default for + /// retail's decompiled kill/death-notification color + /// (HandleKillerNotificationEvent @0x0056C410), or 0x06 + /// (the generic Combat slot) when no more specific classification is + /// in hand — the generic-slot choice is a registered approximation of + /// retail's per-message dispatch (register row AP-176). + /// + /// + /// Severity bucket for panel coloring; defaults to + /// . /// public void OnCombatLine( - string text, Combat.CombatLineKind kind = Combat.CombatLineKind.Info, uint logTextType = 0x06u) + string text, uint logTextType, Combat.CombatLineKind kind = Combat.CombatLineKind.Info) { Append(new ChatEntry( Kind: ChatKind.Combat, @@ -400,17 +408,16 @@ public sealed class ChatLog /// (a real incoming Tell carries the sender's player guid). /// /// - /// The retail LogTextType for this self-sent line. When - /// , defaults to 0x04 Speech_Direct_Send - /// for Tell (retail's own-echo "You tell ..." type — cross-check - /// ACE's ChatMessageType.OutgoingTell comment "You tell ...") or - /// 0x0B Social_Send for Channel (the simplified own-send default - /// research doc §3.3 records; the LiveSessionCommandRouter production - /// caller overrides this with the precise per-channel-bit value from - /// instead of relying on - /// this fallback). + /// The retail LogTextType for this self-sent line — required; + /// no production caller relies on a default (the old null-fallback + /// ternary is retired). Pass 0x04 Speech_Direct_Send for Tell + /// (retail's own-echo "You tell ..." type — cross-check ACE's + /// ChatMessageType.OutgoingTell comment "You tell ...") or, for + /// Channel, the precise per-channel-bit value from + /// — the + /// LiveSessionCommandRouter production caller does exactly this. /// - public void OnSelfSent(ChatKind kind, string text, string targetOrChannel = "", uint? logTextType = null) + public void OnSelfSent(ChatKind kind, string text, uint logTextType, string targetOrChannel = "") { Append(new ChatEntry( Kind: kind, @@ -425,7 +432,7 @@ public sealed class ChatLog ChannelId: 0) { ChannelName = kind == ChatKind.Channel ? targetOrChannel : "", - LogTextType = logTextType ?? (kind == ChatKind.Tell ? 0x04u : 0x0Bu), + LogTextType = logTextType, }); } diff --git a/src/AcDream.Core/Chat/CombatChatTranslator.cs b/src/AcDream.Core/Chat/CombatChatTranslator.cs index 73d2304b..40b2c001 100644 --- a/src/AcDream.Core/Chat/CombatChatTranslator.cs +++ b/src/AcDream.Core/Chat/CombatChatTranslator.cs @@ -115,11 +115,13 @@ public sealed class CombatChatTranslator : IDisposable // grows that field, append " Critical hit." here. "", FormatAttackConditionsSuffix(0)); - // Combat_Self (0x16): retail squelch-checks the attacker's OWN - // outgoing-hit notification against ChatMessageType.CombatSelf — - // references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:162-163 - // (GameEventAttackerNotification, "You hit X..."). - _chat.OnCombatLine(line, CombatLineKind.Info, logTextType: 0x16u); + // Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred — + // ClientCombatSystem::HandleAttackerNotificationEvent @0x0056B420 + // sets type 0x16 at @0x0056B761, the binary's only 0x16 producer + // for this notification family. ACE's ChatMessageType.CombatSelf + // comment (Player_Combat.cs:162-163, GameEventAttackerNotification, + // "You hit X...") corroborates. + _chat.OnCombatLine(line, logTextType: (uint)RetailLogTextType.CombatSelf, kind: CombatLineKind.Info); } private void HandleDamageTaken(CombatState.DamageIncoming e) @@ -142,33 +144,40 @@ public sealed class CombatChatTranslator : IDisposable sb.Append('.'); if (e.Critical) sb.Append(" Critical hit."); sb.Append(FormatAttackConditionsSuffix(0)); - // Combat_Enemy (0x15): retail squelch-checks the defender's - // incoming-hit notification against ChatMessageType.CombatEnemy — - // references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:541 - // (GameEventDefenderNotification, "X hit you..."). - _chat.OnCombatLine(sb.ToString(), CombatLineKind.Warning, logTextType: 0x15u); + // Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred — + // ClientCombatSystem::HandleDefenderNotificationEvent @0x0056C920 + // sets type 0x15 at @0x0056D4B4, the binary's only 0x15 producer + // for this notification family. ACE's ChatMessageType.CombatEnemy + // comment (Player_Combat.cs:541, GameEventDefenderNotification, + // "X hit you...") corroborates. + _chat.OnCombatLine(sb.ToString(), logTextType: (uint)RetailLogTextType.CombatEnemy, kind: CombatLineKind.Warning); } private void HandleMissedOutgoing(string defenderName) { // chat.rs:286-291 — EvasionAttackerNotification: // "{} evaded your attack." - // Combat_Self (0x16): this is about the LOCAL PLAYER'S OWN attack - // missing — retail squelch-checks GameEventEvasionAttackerNotification - // against CombatSelf, same family as the hit-dealt line above — - // references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:150. - _chat.OnCombatLine($"{defenderName} evaded your attack.", CombatLineKind.Info, logTextType: 0x16u); + // Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred — + // ClientCombatSystem::HandleEvasionAttackerNotificationEvent + // @0x0056C7A0 sets type 0x16 at @0x0056C870, the binary's only + // 0x16 producer for this notification family (same slot as the + // hit-dealt line above). ACE's ChatMessageType.CombatSelf comment + // (Player_Combat.cs:150, GameEventEvasionAttackerNotification) + // corroborates. + _chat.OnCombatLine($"{defenderName} evaded your attack.", logTextType: (uint)RetailLogTextType.CombatSelf, kind: CombatLineKind.Info); } private void HandleEvadedIncoming(string attackerName) { // chat.rs:292-297 — EvasionDefenderNotification: // "You evaded {}'s attack." - // Combat_Enemy (0x15): this is about an ENEMY'S attack (that the - // local player evaded) — retail squelch-checks - // GameEventEvasionDefenderNotification against CombatEnemy — - // references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:345. - _chat.OnCombatLine($"You evaded {attackerName}'s attack.", CombatLineKind.Info, logTextType: 0x15u); + // Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred — + // ClientCombatSystem::HandleEvasionDefenderNotificationEvent + // @0x0056C620 sets type 0x15 at @0x0056C710, the binary's only + // 0x15 producer for this notification family. ACE's + // ChatMessageType.CombatEnemy comment (Player_Combat.cs:345, + // GameEventEvasionDefenderNotification) corroborates. + _chat.OnCombatLine($"You evaded {attackerName}'s attack.", logTextType: (uint)RetailLogTextType.CombatEnemy, kind: CombatLineKind.Info); } private void HandleKillLanded(string victimName, uint victimGuid) @@ -185,7 +194,7 @@ public sealed class CombatChatTranslator : IDisposable // both via ClientCombatSystem::HandleKillerNotificationEvent // @0x0056C410) calls AddTextToScroll(..., 0, 1, 0) — see // ChatLog.OnPlayerKilled's identical citation. - _chat.OnCombatLine($"You killed {victimName}.", CombatLineKind.Info, logTextType: 0x00u); + _chat.OnCombatLine($"You killed {victimName}.", logTextType: 0x00u, kind: CombatLineKind.Info); } // ── Formatters (ported VERBATIM from chat.rs:561-595) ─────────────────── diff --git a/src/AcDream.Core/Chat/LegacyChannelChatType.cs b/src/AcDream.Core/Chat/LegacyChannelChatType.cs index e77e16e1..4e4e17db 100644 --- a/src/AcDream.Core/Chat/LegacyChannelChatType.cs +++ b/src/AcDream.Core/Chat/LegacyChannelChatType.cs @@ -16,62 +16,94 @@ namespace AcDream.Core.Chat; /// /// /// +/// Opus review correction, 2026-08-09. The catch-all and two named +/// cases below were WRONG in the original CH1 port. Binary Ninja renders +/// retail's neg esi; sbb esi, esi idiom — a branchless "esi = (cond) +/// ? -1 : 0" pattern used to pick between Channel (0x08) and +/// Channel_Send (0x09) — as the pseudo-C expression esi - esi, +/// which trivially evaluates to 0 and hides the real values. The +/// correction was made by decoding the raw bytes at the PDB-paired binary: +/// the HEAR branch's sbb site is at VA 0x00570F0A (mask +/// -6 maps to 0x08 Channel), and the SEND branch's is at VA +/// 0x00570D4F (mask -5 maps to 0x09 Channel_Send). +/// ACE's PDB-sourced Channel enum corroborates: Abuse (bit +/// 0x0001) is retail's ONLY producer of 0x0E, Help (bit +/// 0x0400) is the only producer of 0x0F, and every other +/// unmatched bit (the admin/audit/advocate/QA/sentinel/town family) resolves +/// to the plain Channel/Channel_Send pair, not Abuse's 0x0E. +/// +/// +/// /// Per-bit findings from the decompiled dispatch (not all bits behave -/// the same for own-send — only Patron/Vassal/Follower get a distinct -/// send-color; Fellowship, Co-Vassals, Allegiance Broadcast, and the -/// generic/admin catch-all use the SAME type for hear and send): +/// the same for own-send — only Patron/Vassal/Follower and the +/// FellowBroadcast bit get a distinct send-color; Fellowship, Co-Vassals, +/// and Allegiance Broadcast use the SAME type for hear and send): /// /// public static class LegacyChannelChatType { /// - /// Resolve (the ChannelBroadcast - /// wire channel id, a single flag bit) to a LogTextType. + /// Resolve (the ChannelBroadcast + /// wire channel id) to a LogTextType. + /// is compared with == against the whole value — it must be a + /// SINGLE flag bit, matching retail's per-bit dispatch. A multi-bit id + /// (e.g. ACE's Channel.AllBroadcast = 0x401, which spans + /// both the Help bit 0x400 and the Abuse bit 0x1) does not + /// equal any single named arm and falls through to the generic + /// catch-all rather than resolving to either bit's own type. /// - /// The wire channel id / bitflag. + /// The wire channel id / bitflag. /// /// when this is the local player's own outgoing /// message (retail's m_buffer->m_len == 1 self-sentinel /// branch); when hearing another sender. /// - public static uint Resolve(uint channelBit, bool ownSend) => channelBit switch + public static uint Resolve(uint channelId, bool ownSend) => channelId switch { + // Abuse: retail's ONLY 0x0E producer. pc:00570f1d (hear) / + // pc:00570d62 (send) — both compute the same constant 0xe, but ONLY + // when ebp == 0x1; every other unmatched bit takes the sbb-derived + // Channel/Channel_Send pair below, not this arm. + 0x0001u => (uint)RetailLogTextType.Abuse, + + // Help: the single named non-family bit inside the generic bucket. + // Both branches (hear label_570f0a and send label_570d4f) test + // `ebp != 0x400` with the identical 0xe/0xf split against Abuse — + // 0x400 gets its OWN color where the sbb catch-all handles everyone + // else. + 0x0400u => (uint)RetailLogTextType.Help, + // Fellowship: same type ("[Fellowship] ...") for hear and send. // pc:00570e48 (hear, m_buffer_5=0x13) / pc:00570d08 (send, 0x13). - 0x0800u => 0x13u, + 0x0800u => (uint)RetailLogTextType.Fellowship, // Patron / Vassal / Follower(Monarch): hear = Social (0xA, - // "Your patron/vassal/follower X tells you..."); OWN send = - // Social_Send (0xB, "You say to your patron/vassal/follower..."). + // "Your patron/vassal/follower X tells you..."); OWN send = Social_ + // Send (0xB, "You say to your patron/vassal/follower..."). // pc:00570e50/00570e58 (Patron/Vassal hear, 0xa) + pc:00570e40 // (Follower hear, 0xa); pc:00570c07/00570c21 (all three own-send // via the shared label_570c21, 0xb). - 0x1000u => ownSend ? 0x0Bu : 0x0Au, // Patron - 0x2000u => ownSend ? 0x0Bu : 0x0Au, // Vassal - 0x4000u => ownSend ? 0x0Bu : 0x0Au, // Follower / Monarch + 0x1000u or 0x2000u or 0x4000u => + ownSend ? (uint)RetailLogTextType.SocialSend : (uint)RetailLogTextType.Social, // Co-Vassals / Allegiance Broadcast: same type for hear and send. // pc:00571025/00571014 (hear, 0xa) / pc:00570e17/00570df7 (send, 0xa). - 0x1000000u => 0x0Au, // Co-Vassals - 0x2000000u => 0x0Au, // Allegiance Broadcast + 0x1000000u or 0x2000000u => (uint)RetailLogTextType.Social, - // Unnamed bit — no producer traced, but retail's own dispatch - // assigns it 0x13 (Fellowship's slot) for both hear and send. - // pc:00570d43 (send, 0x13); the hear branch mirrors it via the - // same ebp==0x4000000 special case one level up in the dispatch. - 0x4000000u => 0x13u, + // FellowBroadcast: the sbb idiom's ebp==0x4000000 special case picks + // Fellowship's OWN-SEND slot (0x13) but the plain sbb Channel value + // (0x08) for hear — pc:00570d43 (send, 0x13). This is the one + // unnamed bit that behaves like neither a pure catch-all nor a pure + // Fellowship alias. + 0x4000000u => + ownSend ? (uint)RetailLogTextType.Fellowship : (uint)RetailLogTextType.Channel, - // The single named non-family bit inside the generic catch-all: - // 0x400 gets its OWN color (Help, 0xF) where every other - // unmatched bit gets the catch-all (Abuse, 0xE). Both branches - // (hear label_570f0a and send label_570d4f) test `ebp != 0x400` - // with the identical 0xe/0xf split. - 0x0400u => 0x0Fu, - - // Generic/admin/audit/sentinel catch-all: retail's - // " says on the X channel" / "You say on the X channel" - // template, color 0xE (Abuse). pc:00570f1d (hear) / pc:00570d62 - // (send) — both compute the same constant 0xe. - _ => 0x0Eu, + // Generic catch-all: the sbb-derived Channel (hear, 0x08) / + // Channel_Send (own-send, 0x09) pair — Admin, Audit, Advocate, QA, + // Sentinel, town channels, and every other unmatched bit. This is + // the branch Binary Ninja's `esi - esi` mis-rendering hid; the real + // masks are decoded from the raw bytes at VA 0x00570F0A (hear, + // mask -6 -> 0x08) and VA 0x00570D4F (send, mask -5 -> 0x09). + _ => ownSend ? (uint)RetailLogTextType.ChannelSend : (uint)RetailLogTextType.Channel, }; } diff --git a/src/AcDream.Core/Chat/RetailLogTextType.cs b/src/AcDream.Core/Chat/RetailLogTextType.cs new file mode 100644 index 00000000..2a51830d --- /dev/null +++ b/src/AcDream.Core/Chat/RetailLogTextType.cs @@ -0,0 +1,87 @@ +namespace AcDream.Core.Chat; + +/// +/// The 34-value retail wire LogTextType index space (the same integer +/// ACE calls ChatMessageType). Values only — no color, no presentation +/// data; AcDream.Core stays presentation-free per Code Structure Rule 2. +/// The actual color lookup lives in +/// AcDream.UI.Abstractions.Panels.Chat.RetailChatColorTable, which is +/// keyed by these same numeric values. +/// +/// +/// Names 0x00-0x1F are retail's own strings from +/// LogTextTypeEnumMapper::LogTextTypeToString @0x006AFF90 (a literal +/// switch over the wire byte). Names for 0x1A, 0x1B-0x1E, +/// 0x20, and 0x21 are NOT retail strings — retail's own mapper +/// returns "Unknown" for all of them; the enum member names for those six +/// slots are acdream's own labels for constants that are otherwise identified +/// only by producer/consumer site (see +/// docs/research/2026-08-09-chat-retail-color-table.md §2.1-2.2 for the +/// full derivation and per-slot evidence). +/// +/// +public enum RetailLogTextType : uint +{ + /// Default-fill slot; retail's own unfilled-slot default. + Default = 0x00, + All = 0x01, + Speech = 0x02, + Tell = 0x03, + SpeechDirectSend = 0x04, + System = 0x05, + Combat = 0x06, + Magic = 0x07, + Channel = 0x08, + ChannelSend = 0x09, + Social = 0x0A, + SocialSend = 0x0B, + Emote = 0x0C, + Advancement = 0x0D, + Abuse = 0x0E, + Help = 0x0F, + Appraisal = 0x10, + Spellcasting = 0x11, + Allegiance = 0x12, + Fellowship = 0x13, + WorldBroadcast = 0x14, + CombatEnemy = 0x15, + CombatSelf = 0x16, + Recall = 0x17, + Craft = 0x18, + Salvaging = 0x19, + + /// + /// acdream label — retail's own mapper has no string for this slot. + /// Client-local text that never reaches the wire: command-parser + /// errors, the cant_jump_* family, most informational command + /// output. See docs/research/2026-08-09-chat-retail-color-table.md + /// §2.2. + /// + ClientLocal = 0x1A, + + /// acdream label — the Turbine "General" community room. + TurbineGeneral = 0x1B, + + /// acdream label — the Turbine "Trade" community room. + TurbineTrade = 0x1C, + + /// acdream label — the Turbine "LFG" community room. + TurbineLFG = 0x1D, + + /// acdream label — the Turbine "Roleplay" community room. + TurbineRoleplay = 0x1E, + + AdminTell = 0x1F, + + /// + /// acdream label — the Turbine "Society" community rooms (all four + /// society sub-rooms share this one slot in retail). + /// + TurbineSociety = 0x20, + + /// + /// acdream label — retail has a color slot and a filter bit for this + /// index but no producer anywhere in the 2013 client; treat as reserved. + /// + Reserved21 = 0x21, +} diff --git a/src/AcDream.Headless/Hosting/HeadlessGameplayOperations.cs b/src/AcDream.Headless/Hosting/HeadlessGameplayOperations.cs index 1ab4f079..b5ca0cc9 100644 --- a/src/AcDream.Headless/Hosting/HeadlessGameplayOperations.cs +++ b/src/AcDream.Headless/Hosting/HeadlessGameplayOperations.cs @@ -223,12 +223,17 @@ internal sealed class HeadlessGameplayOperations session!.SendCastTargetedSpell(targetId, spellId); } - // Client-local text (a bot script's own injected message, never - // reaches the wire), 0x1A — same category as ChatVM.ShowSystemMessage. + // Informational sink — 0x00 Default, NOT 0x1A (corrected 2026-08-09, + // Opus review of 172c6f9a). Same ClientCommandController-output + // category as ChatVM.ShowSystemMessage; retail types the great + // majority of that output green, reserving 0x1A (bright red) for + // genuine refusals. The refusal-vs-informational split lands with + // CH2's producer rewiring (SpewBox routing) — see + // docs/research/2026-08-09-chat-retail-interface-text.md §7.2. public void DisplayMessage(string message) => RequireRuntime().CommunicationOwner.Chat.OnSystemMessage( message, - chatType: 0x1Au); + chatType: 0x00u); public void IncrementBusy() => RequireRuntime().ActionOwner.Transactions diff --git a/src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs b/src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs index aecacdc5..9ee54b3c 100644 --- a/src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs +++ b/src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs @@ -113,12 +113,16 @@ public sealed class ChatVM : IDisposable /// local feedback without round-tripping the server. /// /// - /// LogTextType 0x1A: this text never reaches the wire — the - /// same "client-local text" category as retail's own command-parser - /// errors and cant_jump_* strings (research doc - /// docs/research/2026-08-09-chat-retail-color-table.md §2.2). + /// LogTextType 0x00 Default, NOT 0x1A (corrected 2026-08-09, + /// Opus review of 172c6f9a). This sink is ClientCommandController's + /// general-purpose output — @version, /loc, friends list, usage lines — + /// and retail types the great majority of that informational command + /// output 0x00, reserving 0x1A (bright red) for genuine + /// refusals/errors. The refusal-vs-informational split lands with CH2's + /// producer rewiring (SpewBox routing) — see + /// docs/research/2026-08-09-chat-retail-interface-text.md §7.2. /// - public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x1Au); + public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x00u); /// /// Drain the chat log. Used by the /clear client-side command. diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs index 31030d0d..63f2ef92 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs @@ -45,7 +45,8 @@ public class ChatWindowControllerTests /// send (Type-3) [0x10000019] /// maxmin (Type-3) [0x1000046F] /// - private static (ElementInfo rootInfo, ImportedLayout layout, ChatVM vm) BuildTestTree() + private static (ElementInfo rootInfo, ImportedLayout layout, ChatVM vm) BuildTestTree( + ChatLog? log = null) { var transcriptNode = new ElementInfo { @@ -116,7 +117,7 @@ public class ChatWindowControllerTests root.Children.Add(maxMinNode); var layout = LayoutImporter.Build(root, NoTex, null); - var vm = new ChatVM(new ChatLog()); + var vm = new ChatVM(log ?? new ChatLog()); return (root, layout, vm); } @@ -209,6 +210,34 @@ public class ChatWindowControllerTests Assert.Equal(4, ctrl.TranscriptLayoutBuildCount); } + [Fact] + public void TranscriptLines_OutOfRangeLogTextType_CarriesPreviousLinesColor() + { + // Retail's SetFontColorHelper leaves m_curFontColor UNCHANGED for an + // out-of-range index instead of reverting to a default (research + // doc §3.2) — RetailChatColorTable.TryGetColor returns false for + // any index >= 0x22 and ChatWindowController.GetTranscriptLines + // carries the prior line's resolved color forward. Three entries; + // the middle one uses 0x22 (one past the last real retail slot, + // 0x21) so its rendered color must equal the first line's, not + // the third's. + var log = new ChatLog(); + var (rootInfo, layout, vm) = BuildTestTree(log); + var bus = new CaptureBus(); + var ctrl = ChatWindowController.Bind( + rootInfo, layout, vm, () => bus, null, null, NoTex)!; + + log.OnSystemMessage("first", chatType: 0x05u); // System, colorBrightPurple + log.OnSystemMessage("middle", chatType: 0x22u); // out of range — carries 0x05's color + log.OnSystemMessage("third", chatType: 0x00u); // Default, colorGreen + + IReadOnlyList lines = ctrl.Transcript.LinesProvider(); + + Assert.Equal(3, lines.Count); + Assert.Equal(lines[0].Color, lines[1].Color); + Assert.NotEqual(lines[0].Color, lines[2].Color); + } + // ── Test 4: Input.OnSubmit publishes SendChatCmd via the capture bus ───── [Fact] diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index de77452c..0eaa3e36 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -155,8 +155,10 @@ public sealed class GameEventWiringTests Assert.Equal(ChatKind.Channel, entry.Kind); Assert.Equal("Alice", entry.Sender); // channelId 42 (0x2A) matches no named legacy bit — generic - // admin/audit catch-all (LegacyChannelChatType.Resolve → 0x0E). - Assert.Equal(0x0Eu, entry.LogTextType); + // admin/audit catch-all, hear branch (LegacyChannelChatType.Resolve + // → 0x08 Channel; corrected 2026-08-09, Opus review of 172c6f9a — + // was wrongly 0x0E Abuse, retail's ONLY 0x0E producer is bit 0x0001). + Assert.Equal(0x08u, entry.LogTextType); } [Fact] diff --git a/tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs b/tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs index ae3e9b8d..c8ec32e4 100644 --- a/tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs +++ b/tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs @@ -10,8 +10,8 @@ public sealed class ChatCommandTargetStateTests var chat = new ChatLog(); using var targets = new ChatCommandTargetState(chat); - chat.OnTellReceived("Bestie", "incoming", 0x50000001u); - chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith"); + chat.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u); + chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith"); Assert.Equal("Bestie", targets.LastIncomingTellSender); Assert.Equal("Caith", targets.LastOutgoingTellTarget); @@ -22,8 +22,8 @@ public sealed class ChatCommandTargetStateTests { var chat = new ChatLog(); using var targets = new ChatCommandTargetState(chat); - chat.OnTellReceived("Bestie", "incoming", 0x50000001u); - chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith"); + chat.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u); + chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith"); targets.ResetSession(); @@ -40,7 +40,7 @@ public sealed class ChatCommandTargetStateTests targets.Dispose(); targets.Dispose(); - chat.OnTellReceived("After", "ignored", 0x50000002u); + chat.OnTellReceived("After", "ignored", 0x50000002u, logTextType: 0x03u); Assert.Null(targets.LastIncomingTellSender); Assert.Null(targets.LastOutgoingTellTarget); diff --git a/tests/AcDream.Core.Tests/Chat/ChatLogLocalGuidTests.cs b/tests/AcDream.Core.Tests/Chat/ChatLogLocalGuidTests.cs index 2c5dd3f4..caaa3f5b 100644 --- a/tests/AcDream.Core.Tests/Chat/ChatLogLocalGuidTests.cs +++ b/tests/AcDream.Core.Tests/Chat/ChatLogLocalGuidTests.cs @@ -20,7 +20,7 @@ public sealed class ChatLogLocalGuidTests log.SetLocalPlayerGuid(0x5000_000A); log.OnLocalSpeech("+Acdream", "hello world", - senderGuid: 0x5000_000A, isRanged: false); + senderGuid: 0x5000_000A, isRanged: false, logTextType: 0x02u); var entry = log.Snapshot()[0]; Assert.Equal(ChatKind.LocalSpeech, entry.Kind); @@ -35,7 +35,7 @@ public sealed class ChatLogLocalGuidTests log.SetLocalPlayerGuid(0x5000_000A); log.OnLocalSpeech("Caith", "hi", - senderGuid: 0x5000_0042, isRanged: false); + senderGuid: 0x5000_0042, isRanged: false, logTextType: 0x02u); Assert.Equal("Caith", log.Snapshot()[0].Sender); } @@ -48,7 +48,7 @@ public sealed class ChatLogLocalGuidTests // arrive with sender="" before the player has a guid. var log = new ChatLog(); log.OnLocalSpeech("", "anyone home?", - senderGuid: 0u, isRanged: true); + senderGuid: 0u, isRanged: true, logTextType: 0x02u); Assert.Equal("You", log.Snapshot()[0].Sender); Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind); @@ -62,13 +62,13 @@ public sealed class ChatLogLocalGuidTests const uint newGuid = 0x50000002u; log.SetLocalPlayerGuid(oldGuid); log.OnSystemMessage("session boundary", 1u); - log.OnLocalSpeech("Old", "before", oldGuid, isRanged: false); + log.OnLocalSpeech("Old", "before", oldGuid, isRanged: false, logTextType: 0x02u); log.ResetSessionIdentity(); log.OnSystemMessage("session boundary", 1u); - log.OnLocalSpeech("Old", "after", oldGuid, isRanged: false); + log.OnLocalSpeech("Old", "after", oldGuid, isRanged: false, logTextType: 0x02u); log.SetLocalPlayerGuid(newGuid); - log.OnLocalSpeech("New", "new", newGuid, isRanged: false); + log.OnLocalSpeech("New", "new", newGuid, isRanged: false, logTextType: 0x02u); ChatEntry[] entries = log.Snapshot(); Assert.Equal(5, entries.Length); diff --git a/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs b/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs index 948548a7..485c36a0 100644 --- a/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs +++ b/tests/AcDream.Core.Tests/Chat/ChatLogTests.cs @@ -13,7 +13,7 @@ public sealed class ChatLogTests ChatEntry? seen = null; log.EntryAppended += e => seen = e; - log.OnLocalSpeech("Alice", "hi", 0xAA, isRanged: false); + log.OnLocalSpeech("Alice", "hi", 0xAA, isRanged: false, logTextType: 0x02u); Assert.Equal(1, log.Count); Assert.NotNull(seen); @@ -26,7 +26,7 @@ public sealed class ChatLogTests public void OnLocalSpeech_Ranged_SetsRangedKind() { var log = new ChatLog(); - log.OnLocalSpeech("Bob", "SHOUT", 0xBB, isRanged: true); + log.OnLocalSpeech("Bob", "SHOUT", 0xBB, isRanged: true, logTextType: 0x02u); Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind); } @@ -44,7 +44,7 @@ public sealed class ChatLogTests public void OnTellReceived_SetsTellKind() { var log = new ChatLog(); - log.OnTellReceived("Alice", "psst", 0xAA); + log.OnTellReceived("Alice", "psst", 0xAA, logTextType: 0x03u); Assert.Equal(ChatKind.Tell, log.Snapshot()[0].Kind); } @@ -62,7 +62,7 @@ public sealed class ChatLogTests public void OnSelfSent_EchoesOutbound() { var log = new ChatLog(); - log.OnSelfSent(ChatKind.Tell, "hey", targetOrChannel: "Alice"); + log.OnSelfSent(ChatKind.Tell, "hey", logTextType: 0x04u, targetOrChannel: "Alice"); var e = log.Snapshot()[0]; Assert.Equal("Alice", e.Sender); Assert.Equal("hey", e.Text); @@ -72,10 +72,10 @@ public sealed class ChatLogTests public void RingBuffer_DropsOldestBeyondCapacity() { var log = new ChatLog(maxEntries: 3); - log.OnLocalSpeech("A", "1", 0, false); - log.OnLocalSpeech("B", "2", 0, false); - log.OnLocalSpeech("C", "3", 0, false); - log.OnLocalSpeech("D", "4", 0, false); + log.OnLocalSpeech("A", "1", 0, false, logTextType: 0x02u); + log.OnLocalSpeech("B", "2", 0, false, logTextType: 0x02u); + log.OnLocalSpeech("C", "3", 0, false, logTextType: 0x02u); + log.OnLocalSpeech("D", "4", 0, false, logTextType: 0x02u); var snap = log.Snapshot(); Assert.Equal(3, snap.Length); @@ -87,7 +87,7 @@ public sealed class ChatLogTests public void Clear_EmptiesBuffer() { var log = new ChatLog(); - log.OnLocalSpeech("A", "1", 0, false); + log.OnLocalSpeech("A", "1", 0, false, logTextType: 0x02u); log.Clear(); Assert.Equal(0, log.Count); } @@ -172,7 +172,7 @@ public sealed class ChatLogTests // ranged shout). Substitute "You" so the chat line reads // "You: hello" instead of ": hello". var log = new ChatLog(); - log.OnLocalSpeech(sender: "", text: "hello", senderGuid: 0, isRanged: false); + log.OnLocalSpeech(sender: "", text: "hello", senderGuid: 0, isRanged: false, logTextType: 0x02u); var e = log.Snapshot()[0]; Assert.Equal("You", e.Sender); Assert.Equal("hello", e.Text); @@ -182,7 +182,7 @@ public sealed class ChatLogTests public void OnLocalSpeech_NonEmptySender_KeepsAsIs() { var log = new ChatLog(); - log.OnLocalSpeech(sender: "Alice", text: "hi", senderGuid: 0xAA, isRanged: false); + log.OnLocalSpeech(sender: "Alice", text: "hi", senderGuid: 0xAA, isRanged: false, logTextType: 0x02u); Assert.Equal("Alice", log.Snapshot()[0].Sender); } @@ -192,7 +192,7 @@ public sealed class ChatLogTests public void OnCombatLine_DefaultsInfoKind_TagsEntryAsCombat() { var log = new ChatLog(); - log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%)."); + log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%).", logTextType: 0x06u); var e = log.Snapshot()[0]; Assert.Equal(ChatKind.Combat, e.Kind); Assert.Equal(CombatLineKind.Info, e.CombatKind); @@ -204,21 +204,21 @@ public sealed class ChatLogTests { var log = new ChatLog(); log.OnCombatLine("Mosswart hit you for 8 fire damage to your chest.", - CombatLineKind.Warning); + logTextType: 0x06u, kind: CombatLineKind.Warning); Assert.Equal(CombatLineKind.Warning, log.Snapshot()[0].CombatKind); log.OnCombatLine("Attack sequence finished with WeenieError 0x1234.", - CombatLineKind.Error); + logTextType: 0x06u, kind: CombatLineKind.Error); Assert.Equal(CombatLineKind.Error, log.Snapshot()[1].CombatKind); } // ── Campaign CH slice CH1: LogTextType ingestion-site mapping ────────── [Fact] - public void OnLocalSpeech_DefaultsLogTextType_ToSpeech() + public void OnLocalSpeech_ExplicitLogTextType_PinsSpeechValue() { var log = new ChatLog(); - log.OnLocalSpeech("Alice", "hi", 0xAA, isRanged: false); + log.OnLocalSpeech("Alice", "hi", 0xAA, isRanged: false, logTextType: 0x02u); Assert.Equal(0x02u, log.Snapshot()[0].LogTextType); } @@ -275,10 +275,10 @@ public sealed class ChatLogTests } [Fact] - public void OnTellReceived_DefaultsLogTextType_ToTell() + public void OnTellReceived_ExplicitLogTextType_PinsTellValue() { var log = new ChatLog(); - log.OnTellReceived("Alice", "psst", 0xAA); + log.OnTellReceived("Alice", "psst", 0xAA, logTextType: 0x03u); Assert.Equal(0x03u, log.Snapshot()[0].LogTextType); } @@ -291,18 +291,18 @@ public sealed class ChatLogTests } [Fact] - public void OnSelfSent_Tell_DefaultsLogTextType_ToSpeechDirectSend() + public void OnSelfSent_Tell_ExplicitLogTextType_PinsSpeechDirectSendValue() { var log = new ChatLog(); - log.OnSelfSent(ChatKind.Tell, "hey", targetOrChannel: "Alice"); + log.OnSelfSent(ChatKind.Tell, "hey", logTextType: 0x04u, targetOrChannel: "Alice"); Assert.Equal(0x04u, log.Snapshot()[0].LogTextType); } [Fact] - public void OnSelfSent_Channel_DefaultsLogTextType_ToSocialSend() + public void OnSelfSent_Channel_ExplicitLogTextType_PinsSocialSendValue() { var log = new ChatLog(); - log.OnSelfSent(ChatKind.Channel, "hi all", targetOrChannel: "Fellowship"); + log.OnSelfSent(ChatKind.Channel, "hi all", logTextType: 0x0Bu, targetOrChannel: "Fellowship"); Assert.Equal(0x0Bu, log.Snapshot()[0].LogTextType); } @@ -310,7 +310,7 @@ public sealed class ChatLogTests public void OnSelfSent_ExplicitLogTextType_Overrides() { var log = new ChatLog(); - log.OnSelfSent(ChatKind.Channel, "hi all", targetOrChannel: "Fellowship", logTextType: 0x13u); + log.OnSelfSent(ChatKind.Channel, "hi all", logTextType: 0x13u, targetOrChannel: "Fellowship"); Assert.Equal(0x13u, log.Snapshot()[0].LogTextType); } @@ -324,12 +324,15 @@ public sealed class ChatLogTests // Co-Vassals / Allegiance Broadcast. [InlineData(0x1000000u, "Co-Vassals", 0x0Au)] [InlineData(0x2000000u, "Allegiance Broadcast", 0x0Au)] - // Unnamed bit reuses Fellowship's slot. - [InlineData(0x4000000u, "?", 0x13u)] + // FellowBroadcast — hear is the plain Channel slot (corrected + // 2026-08-09, Opus review of 172c6f9a — was wrongly 0x13). + [InlineData(0x4000000u, "?", 0x08u)] // The one named non-family bit inside the generic bucket (Help). [InlineData(0x0400u, "Help", 0x0Fu)] - // Generic admin/audit/sentinel catch-all. - [InlineData(0x0900u, "Audit", 0x0Eu)] + // Generic admin/audit/sentinel catch-all — Channel (hear), NOT Abuse + // (corrected 2026-08-09, Opus review of 172c6f9a — Abuse is retail's + // ONLY 0x0E producer, bit 0x0001, and this test's 0x0900 doesn't hit it). + [InlineData(0x0900u, "Audit", 0x08u)] public void OnChannelBroadcast_DerivesLogTextType_FromLegacyChannelBit( uint channelBit, string channelName, uint expectedLogTextType) { @@ -359,10 +362,10 @@ public sealed class ChatLogTests } [Fact] - public void OnCombatLine_DefaultLogTextType_IsGenericCombat() + public void OnCombatLine_ExplicitLogTextType_PinsGenericCombatValue() { var log = new ChatLog(); - log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%)."); + log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%).", logTextType: 0x06u); Assert.Equal(0x06u, log.Snapshot()[0].LogTextType); } } diff --git a/tests/AcDream.Core.Tests/Chat/LegacyChannelChatTypeTests.cs b/tests/AcDream.Core.Tests/Chat/LegacyChannelChatTypeTests.cs index 32d58361..e8b128cc 100644 --- a/tests/AcDream.Core.Tests/Chat/LegacyChannelChatTypeTests.cs +++ b/tests/AcDream.Core.Tests/Chat/LegacyChannelChatTypeTests.cs @@ -9,7 +9,15 @@ namespace AcDream.Core.Tests.Chat; /// ClientCommunicationSystem::Handle_Communication__ChannelBroadcast /// @0x00570B90 — both the HEAR (someone else's message) and OWN-SEND /// (the local player's own outgoing message) branches, which diverge for -/// exactly three bits (Patron/Vassal/Follower). +/// Patron/Vassal/Follower and the unnamed FellowBroadcast bit. +/// +/// +/// Opus review, 2026-08-09: the catch-all and the Abuse/Help/FellowBroadcast +/// cases were corrected after decoding the raw sbb-idiom bytes at VA +/// 0x00570F0A (hear) / 0x00570D4F (send) — Binary Ninja's +/// pseudo-C had rendered the idiom as the trivial esi - esi (always +/// 0), hiding the real Channel (0x08) / Channel_Send (0x09) values. +/// /// public sealed class LegacyChannelChatTypeTests { @@ -17,23 +25,51 @@ public sealed class LegacyChannelChatTypeTests [InlineData(0x0800u, 0x13u)] // Fellowship [InlineData(0x1000000u, 0x0Au)] // Co-Vassals [InlineData(0x2000000u, 0x0Au)] // Allegiance Broadcast - [InlineData(0x4000000u, 0x13u)] // unnamed bit - [InlineData(0x0400u, 0x0Fu)] // Help - [InlineData(0x0100u, 0x0Eu)] // generic/admin catch-all - [InlineData(0xDEADu, 0x0Eu)] // any other unmatched bit - public void Resolve_SameForHearAndSend(uint channelBit, uint expected) + public void Resolve_SameForHearAndSend(uint channelId, uint expected) { - Assert.Equal(expected, LegacyChannelChatType.Resolve(channelBit, ownSend: false)); - Assert.Equal(expected, LegacyChannelChatType.Resolve(channelBit, ownSend: true)); + Assert.Equal(expected, LegacyChannelChatType.Resolve(channelId, ownSend: false)); + Assert.Equal(expected, LegacyChannelChatType.Resolve(channelId, ownSend: true)); } [Theory] [InlineData(0x1000u)] // Patron [InlineData(0x2000u)] // Vassal [InlineData(0x4000u)] // Follower / Monarch - public void Resolve_HearIsSocial_SendIsSocialSend(uint channelBit) + public void Resolve_HearIsSocial_SendIsSocialSend(uint channelId) { - Assert.Equal(0x0Au, LegacyChannelChatType.Resolve(channelBit, ownSend: false)); - Assert.Equal(0x0Bu, LegacyChannelChatType.Resolve(channelBit, ownSend: true)); + Assert.Equal(0x0Au, LegacyChannelChatType.Resolve(channelId, ownSend: false)); + Assert.Equal(0x0Bu, LegacyChannelChatType.Resolve(channelId, ownSend: true)); + } + + [Fact] + public void Resolve_Abuse_IsRetailsOnly0xEProducer() + { + // Bit 0x0001 — the ONLY channel bit that resolves to Abuse (0x0E) + // for either hear or send. + Assert.Equal(0x0Eu, LegacyChannelChatType.Resolve(0x0001u, ownSend: false)); + Assert.Equal(0x0Eu, LegacyChannelChatType.Resolve(0x0001u, ownSend: true)); + } + + [Fact] + public void Resolve_Help_IsSameForHearAndSend() + { + Assert.Equal(0x0Fu, LegacyChannelChatType.Resolve(0x0400u, ownSend: false)); + Assert.Equal(0x0Fu, LegacyChannelChatType.Resolve(0x0400u, ownSend: true)); + } + + [Fact] + public void Resolve_FellowBroadcast_HearIsChannel_SendIsFellowship() + { + Assert.Equal(0x08u, LegacyChannelChatType.Resolve(0x4000000u, ownSend: false)); + Assert.Equal(0x13u, LegacyChannelChatType.Resolve(0x4000000u, ownSend: true)); + } + + [Theory] + [InlineData(0x0100u)] // generic/admin catch-all + [InlineData(0xDEADu)] // any other unmatched bit + public void Resolve_CatchAll_HearIsChannel_SendIsChannelSend(uint channelId) + { + Assert.Equal(0x08u, LegacyChannelChatType.Resolve(channelId, ownSend: false)); + Assert.Equal(0x09u, LegacyChannelChatType.Resolve(channelId, ownSend: true)); } } diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCommunicationStateTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCommunicationStateTests.cs index f85806be..fcbada7f 100644 --- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCommunicationStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCommunicationStateTests.cs @@ -51,7 +51,7 @@ public sealed class RuntimeCommunicationStateTests var observer = new RecordingObserver(); using IDisposable subscription = state.Events.Subscribe(observer); - state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u); + state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u, logTextType: 0x03u); RuntimeCommunicationEvent delta = Assert.Single(observer.Events); Assert.Equal(1UL, delta.Sequence); @@ -108,8 +108,8 @@ public sealed class RuntimeCommunicationStateTests public void SessionResetsClearScopedStateWithoutClearingTranscript() { using var state = new RuntimeCommunicationState(); - state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u); - state.Chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith"); + state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u, logTextType: 0x03u); + state.Chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith"); state.TurbineChat.OnChannelsReceived( 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u); state.Friends.Apply(new FriendsUpdate( @@ -159,7 +159,7 @@ public sealed class RuntimeCommunicationStateTests using var first = new RuntimeCommunicationState(); using var second = new RuntimeCommunicationState(); - first.Chat.OnTellReceived("OnlyFirst", "hello", 0x50000001u); + first.Chat.OnTellReceived("OnlyFirst", "hello", 0x50000001u, logTextType: 0x03u); Assert.Equal(1, first.View.Count); Assert.Equal(0, second.View.Count); diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs index 4f39070b..26f0839e 100644 --- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs +++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeGameplayOwnershipTests.cs @@ -87,11 +87,13 @@ public sealed class RuntimeGameplayOwnershipTests communication.Chat.OnTellReceived( "Sender", "hello", - 0x50000001u); + 0x50000001u, + logTextType: 0x03u); communication.Chat.OnSelfSent( ChatKind.Tell, "reply", - "Recipient"); + logTextType: 0x04u, + targetOrChannel: "Recipient"); communication.TurbineChat.OnChannelsReceived( 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u); communication.Friends.Apply(new FriendsUpdate( diff --git a/tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs b/tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs index 3e94878f..fc315dbc 100644 --- a/tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs +++ b/tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs @@ -180,7 +180,8 @@ public sealed class NoWindowGameRuntimeHostTests "Runtime", "fixture speech", localGuid, - isRanged: false); + isRanged: false, + logTextType: 0x02u); runtime.CharacterOwner.LocalPlayer.OnVitalUpdate( vitalId: 1u, ranks: 100u, diff --git a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs index defd8cf3..749f07cc 100644 --- a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs @@ -18,8 +18,8 @@ public sealed class ChatVMTests public void RecentLines_ReturnsAllEntries_WhenBelowLimit() { var log = new ChatLog(); - log.OnLocalSpeech(sender: "Caith", text: "hello", senderGuid: 0x5000_0001u, isRanged: false); - log.OnLocalSpeech(sender: "Regal", text: "world", senderGuid: 0x5000_0002u, isRanged: false); + log.OnLocalSpeech(sender: "Caith", text: "hello", senderGuid: 0x5000_0001u, isRanged: false, logTextType: 0x02u); + log.OnLocalSpeech(sender: "Regal", text: "world", senderGuid: 0x5000_0002u, isRanged: false, logTextType: 0x02u); var vm = new ChatVM(log, displayLimit: 20); var lines = vm.RecentLines(); @@ -34,7 +34,7 @@ public sealed class ChatVMTests { var log = new ChatLog(); for (int i = 0; i < 30; i++) - log.OnLocalSpeech(sender: "A", text: $"msg{i}", senderGuid: 0x5000_0001u, isRanged: false); + log.OnLocalSpeech(sender: "A", text: $"msg{i}", senderGuid: 0x5000_0001u, isRanged: false, logTextType: 0x02u); var vm = new ChatVM(log, displayLimit: 5); var lines = vm.RecentLines(); @@ -136,24 +136,27 @@ public sealed class ChatVMTests Assert.Empty(vm.RecentLines()); - log.OnLocalSpeech("Caith", "hello", 0x5000_0001u, false); + log.OnLocalSpeech("Caith", "hello", 0x5000_0001u, false, logTextType: 0x02u); var after = vm.RecentLines(); Assert.Single(after); Assert.Equal("Caith says, \"hello\"", after[0]); } [Fact] - public void ShowSystemMessage_UsesClientLocalLogTextType() + public void ShowSystemMessage_UsesDefaultLogTextType() { - // Campaign CH slice CH1: client-local command output (/help, /clear, - // /framerate, /loc, ...) never reaches the wire — retail's own - // client-local text is LogTextType 0x1A (bright red). + // Corrected 2026-08-09, Opus review of 172c6f9a: ShowSystemMessage + // is ClientCommandController's general-purpose informational sink + // (/help, /clear, /framerate, /loc, @version, friends list, ...). + // Retail types the great majority of that command output 0x00 + // Default (green); 0x1A (bright red) is reserved for genuine + // refusals, which land separately with CH2's SpewBox routing. var log = new ChatLog(); var vm = new ChatVM(log); vm.ShowSystemMessage("Unknown command: foo"); var entry = Assert.Single(log.Snapshot()); - Assert.Equal(0x1Au, entry.LogTextType); + Assert.Equal(0x00u, entry.LogTextType); } } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs index c26cb055..ac105f5a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs @@ -280,7 +280,7 @@ public sealed class ChatPanelInputTests { var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived("Bestie", "ping", senderGuid: 0x5000_00AAu); + log.OnTellReceived("Bestie", "ping", senderGuid: 0x5000_00AAu, logTextType: 0x03u); var panel = new ChatPanel(vm); var bus = new RecordingBus(); diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelLayoutTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelLayoutTests.cs index 28813d34..36904453 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelLayoutTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelLayoutTests.cs @@ -99,7 +99,7 @@ public sealed class ChatPanelLayoutTests Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetScrollHereY"); // Append a new entry, render again — auto-scroll should fire. - log.OnLocalSpeech("Caith", "hello", senderGuid: 0xAA, isRanged: false); + log.OnLocalSpeech("Caith", "hello", senderGuid: 0xAA, isRanged: false, logTextType: 0x02u); renderer.Calls.Clear(); panel.Render(ctx, renderer); diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMCombatTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMCombatTests.cs index c9f3433c..42bd5df4 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMCombatTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMCombatTests.cs @@ -34,7 +34,7 @@ public sealed class ChatVMCombatTests var log = new ChatLog(); var vm = new ChatVM(log); log.OnCombatLine("Mosswart hit you for 8 fire damage to your chest.", - CombatLineKind.Warning); + logTextType: 0x06u, kind: CombatLineKind.Warning); var lines = vm.RecentLinesDetailed(); var line = Assert.Single(lines); @@ -48,7 +48,7 @@ public sealed class ChatVMCombatTests { var log = new ChatLog(); var vm = new ChatVM(log); - log.OnLocalSpeech("Alice", "hi", senderGuid: 0xAA, isRanged: false); + log.OnLocalSpeech("Alice", "hi", senderGuid: 0xAA, isRanged: false, logTextType: 0x02u); var line = Assert.Single(vm.RecentLinesDetailed()); Assert.Equal(ChatKind.LocalSpeech, line.Kind); @@ -61,9 +61,9 @@ public sealed class ChatVMCombatTests { var log = new ChatLog(); var vm = new ChatVM(log); - log.OnLocalSpeech("Alice", "hi", senderGuid: 0xAA, isRanged: false); + log.OnLocalSpeech("Alice", "hi", senderGuid: 0xAA, isRanged: false, logTextType: 0x02u); log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%).", - CombatLineKind.Info); + logTextType: 0x06u, kind: CombatLineKind.Info); var panel = new ChatPanel(vm); var bus = new RecordingChatBus(); @@ -74,8 +74,9 @@ public sealed class ChatVMCombatTests // Plain LocalSpeech entry → Text; combat entry → TextColored, now // sourced from RetailChatColorTable (Campaign CH slice CH1) keyed // by LogTextType, not ChatPanel.ColorForCombat's severity bucket. - // OnCombatLine's default logTextType (0x06, generic Combat slot, - // colorDarkRed) applies here since no explicit type was passed. + // The 0x06 generic Combat slot (colorDarkRed) is passed explicitly + // above — a registered approximation of retail's per-message + // dispatch (register row AP-176), not a ChatLog default. Assert.Contains(renderer.Calls, c => c.Method == "Text" && (string?)c.Args[0] == "Alice says, \"hi\""); var coloredCall = Assert.Single( diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMLastTellSenderTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMLastTellSenderTests.cs index 91a3e4cf..b2a4dae0 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMLastTellSenderTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMLastTellSenderTests.cs @@ -17,11 +17,11 @@ public sealed class ChatVMLastTellSenderTests { var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived("Before", "ping", 0x5000_0001u); + log.OnTellReceived("Before", "ping", 0x5000_0001u, logTextType: 0x03u); vm.Dispose(); vm.Dispose(); - log.OnTellReceived("After", "pong", 0x5000_0002u); + log.OnTellReceived("After", "pong", 0x5000_0002u, logTextType: 0x03u); Assert.Equal("Before", vm.LastIncomingTellSender); } @@ -41,7 +41,7 @@ public sealed class ChatVMLastTellSenderTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived(sender: "Bestie", text: "ping", senderGuid: 0x5000_00AAu); + log.OnTellReceived(sender: "Bestie", text: "ping", senderGuid: 0x5000_00AAu, logTextType: 0x03u); Assert.Equal("Bestie", vm.LastIncomingTellSender); } @@ -52,8 +52,8 @@ public sealed class ChatVMLastTellSenderTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived("Bestie", "ping", 0x5000_00AAu); - log.OnTellReceived("Regal", "yo", 0x5000_00BBu); + log.OnTellReceived("Bestie", "ping", 0x5000_00AAu, logTextType: 0x03u); + log.OnTellReceived("Regal", "yo", 0x5000_00BBu, logTextType: 0x03u); Assert.Equal("Regal", vm.LastIncomingTellSender); } @@ -66,8 +66,8 @@ public sealed class ChatVMLastTellSenderTests // /r reply echo: SenderGuid = 0 (no real GUID for ourselves). // Must NOT clobber the captured sender. - log.OnTellReceived("Bestie", "ping", 0x5000_00AAu); - log.OnSelfSent(ChatKind.Tell, "back at you", targetOrChannel: "Bestie"); + log.OnTellReceived("Bestie", "ping", 0x5000_00AAu, logTextType: 0x03u); + log.OnSelfSent(ChatKind.Tell, "back at you", logTextType: 0x04u, targetOrChannel: "Bestie"); Assert.Equal("Bestie", vm.LastIncomingTellSender); } @@ -78,7 +78,7 @@ public sealed class ChatVMLastTellSenderTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnLocalSpeech("Caith", "hello", 0x5000_00CCu, isRanged: false); + log.OnLocalSpeech("Caith", "hello", 0x5000_00CCu, isRanged: false, logTextType: 0x02u); Assert.Null(vm.LastIncomingTellSender); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMRetellAndProvidersTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMRetellAndProvidersTests.cs index 3ed38d82..a1b516f9 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMRetellAndProvidersTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatVMRetellAndProvidersTests.cs @@ -22,7 +22,7 @@ public sealed class ChatVMRetellAndProvidersTests using var second = new ChatVM(log, commandTargets: targets); first.Dispose(); - log.OnTellReceived("Bestie", "incoming", 0x50000001u); + log.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u); Assert.Equal("Bestie", second.LastIncomingTellSender); } @@ -32,8 +32,8 @@ public sealed class ChatVMRetellAndProvidersTests { var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived("Bestie", "incoming", 0x50000001u); - log.OnSelfSent(ChatKind.Tell, "outgoing", targetOrChannel: "Caith"); + log.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u); + log.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith"); Assert.NotNull(vm.LastIncomingTellSender); Assert.NotNull(vm.LastOutgoingTellTarget); @@ -58,7 +58,7 @@ public sealed class ChatVMRetellAndProvidersTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnSelfSent(ChatKind.Tell, "hi there", targetOrChannel: "Caith"); + log.OnSelfSent(ChatKind.Tell, "hi there", logTextType: 0x04u, targetOrChannel: "Caith"); Assert.Equal("Caith", vm.LastOutgoingTellTarget); // Inbound-tell tracker must NOT pick up an outgoing echo — @@ -72,7 +72,7 @@ public sealed class ChatVMRetellAndProvidersTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042); + log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042, logTextType: 0x03u); Assert.Equal("Bestie", vm.LastIncomingTellSender); Assert.Null(vm.LastOutgoingTellTarget); @@ -84,8 +84,8 @@ public sealed class ChatVMRetellAndProvidersTests var log = new ChatLog(); var vm = new ChatVM(log); - log.OnSelfSent(ChatKind.Tell, "hi", targetOrChannel: "Caith"); - log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042); + log.OnSelfSent(ChatKind.Tell, "hi", logTextType: 0x04u, targetOrChannel: "Caith"); + log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042, logTextType: 0x03u); Assert.Equal("Caith", vm.LastOutgoingTellTarget); Assert.Equal("Bestie", vm.LastIncomingTellSender);