fix(chat): CH1 review fixes — sbb-idiom channel catch-all, command-output typing
Applies the Opus review findings on CH1 (172c6f9a), the exact retail chat
color table. Two blockers plus should-fixes/nits, one commit:
BLOCKER 1 — LegacyChannelChatType.Resolve's channel-bit table was wrong.
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
`esi - esi` (always 0), hiding the real values. Corrected by decoding the
raw bytes at the PDB-paired binary: HEAR sbb site VA 0x00570F0A (mask -6 ->
0x08), SEND sbb site VA 0x00570D4F (mask -5 -> 0x09). The generic
admin/audit/sentinel catch-all is Channel/Channel_Send, NOT Abuse (0x0E) —
Abuse is retail's ONLY 0x0E producer (bit 0x0001). The unnamed
FellowBroadcast bit (0x4000000) is hear=Channel(0x08)/send=Fellowship(0x13),
not a flat 0x13. ACE's PDB-sourced Channel enum corroborates. Introduces
`RetailLogTextType`, the 34-value named enum for the wire LogTextType space
(values only, no color — Core stays presentation-free).
BLOCKER 2 — three ChatLog.OnSystemMessage sinks (ChatVM.ShowSystemMessage,
LiveSessionRuntimeFactory's ShowSystemMessage delegate,
HeadlessGameplayOperations.DisplayMessage) were typing ALL
ClientCommandController output 0x1A (bright red), including informational
command output (@version, /loc, friends list, usage lines). Retail types
the great majority of that output 0x00 Default (green) and reserves 0x1A
for genuine refusals/errors. Reverted to 0x00 with a comment noting the
refusal-vs-info split lands with CH2's SpewBox producer rewiring. The five
App composition sites that pass 0x1A for actual refusal text
(InteractionRetainedUiComposition, SessionPlayerComposition) were already
correct and are untouched (aside from converting the literal to the new
enum).
Also: AP-176 divergence-register row for OnWeenieError/OnCombatLine's
single-type approximation of retail's per-code/per-message dispatch; a
carry-forward test for the out-of-range LogTextType color fallback in
ChatWindowController; decomp-confirmed anchors replacing ACE-inferred
citations in CombatChatTranslator and ChatLog.OnPlayerKilled; required
(non-optional) logTextType parameters on OnLocalSpeech/OnTellReceived/
OnCombatLine/OnSelfSent since no production caller relied on a default;
LegacyChannelChatType.Resolve's parameter renamed channelBit -> channelId
with a doc note on multi-bit ids; corrections to the color-table research
doc's §3.3 wire tables; and issue #359 for the pre-existing (not
CH1-introduced) 0x019E PlayerKilled participant-suppression gap retail has
and acdream lacks.
dotnet build clean; full Release suite 11,835 passed / 4 skipped / 0 failed
(11,839 total), up from the CH1 baseline of 11,833/4/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e306c979ae
commit
34d8a3c0e7
31 changed files with 481 additions and 213 deletions
|
|
@ -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
|
retail mixing math or persisted volumes. Once the trigger works, no further
|
||||||
audio-side work is needed.
|
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
|
## #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
|
**Status:** CLOSED 2026-08-08 — root-caused and fixed same session (see the
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -99,7 +99,7 @@ implementer per slice against a pinned contract (per
|
||||||
| Slice | Commit | Suite | Review | User gate |
|
| Slice | Commit | Suite | Review | User gate |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| R1–R4 research | `see docs/research/2026-08-09-chat-retail-*` | — | — | — |
|
| 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 | — | — | — | — |
|
| CH2 interface text | — | — | — | — |
|
||||||
| CH3 side channels | — | — | — | — |
|
| CH3 side channels | — | — | — | — |
|
||||||
| CH4 commands | — | — | — | — |
|
| CH4 commands | — | — | — | — |
|
||||||
|
|
|
||||||
|
|
@ -267,15 +267,26 @@ Anchors are the `ClientCommunicationSystem::Handle_Communication__*` handlers.
|
||||||
`Handle_Communication__ChannelBroadcast` channel-bit → type (the `m_buffer_5`
|
`Handle_Communication__ChannelBroadcast` channel-bit → type (the `m_buffer_5`
|
||||||
variable feeding `AddTextToScroll` at `0x00571169`):
|
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 |
|
| Channel bit | Prefix retail prints | Type |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `0x0001` Abuse | `[<name>]` | `0x0E` — retail's ONLY 0x0E producer (corrected 2026-08-09) |
|
||||||
|
| `0x0400` Help | `[<name>]` | `0x0F` |
|
||||||
| `0x0800` Fellowship | `[Fellowship]` | `0x13` |
|
| `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) |
|
| `0x4000` Follower/Monarch | `Your follower …` | `0x0A` (hear) / `0x0B` (own send) |
|
||||||
| `0x1000000` Co-Vassals | `[Co-Vassals]` | `0x0A` |
|
| `0x1000000` Co-Vassals | `[Co-Vassals]` | `0x0A` |
|
||||||
| `0x2000000` Allegiance Broadcast | `[Allegiance Broadcast]` | `0x0A` |
|
| `0x2000000` Allegiance Broadcast | `[Allegiance Broadcast]` | `0x0A` |
|
||||||
| `0x4000000` | — | `0x13` |
|
| `0x4000000` FellowBroadcast | — | `0x08` (hear) / `0x13` (own send) — corrected 2026-08-09, was wrongly `0x13` for both |
|
||||||
| admin/audit/sentinel channels | `[<name>]` | `0x0E` / `0x0F` |
|
| admin/audit/advocate/QA/sentinel/town catch-all | `[<name>]` | `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
|
Note the split that surprises people: **legacy allegiance-family chat arrives
|
||||||
as `Social` (`0x0A`, yellow) / `Social_Send` (`0x0B`, dark yellow)**, while
|
as `Social` (`0x0A`, yellow) / `Social_Send` (`0x0B`, dark yellow)**, while
|
||||||
|
|
|
||||||
|
|
@ -345,7 +345,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Actions.Selection.SelectedObjectId ?? 0u,
|
d.Actions.Selection.SelectedObjectId ?? 0u,
|
||||||
stackSplitQuantity: d.StackSplitQuantity,
|
stackSplitQuantity: d.StackSplitQuantity,
|
||||||
systemMessage:
|
systemMessage:
|
||||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal),
|
||||||
sendPutItemInContainer: (item, container, placement) =>
|
sendPutItemInContainer: (item, container, placement) =>
|
||||||
session.CurrentSession?.SendPutItemInContainer(
|
session.CurrentSession?.SendPutItemInContainer(
|
||||||
item,
|
item,
|
||||||
|
|
@ -415,7 +415,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
target,
|
target,
|
||||||
spellId),
|
spellId),
|
||||||
displayMessage:
|
displayMessage:
|
||||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal),
|
||||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||||
canSend: () => late.Session.IsInWorld);
|
canSend: () => late.Session.IsInWorld);
|
||||||
|
|
||||||
|
|
@ -753,7 +753,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
iconComposer.GetIcon,
|
iconComposer.GetIcon,
|
||||||
itemInteraction,
|
itemInteraction,
|
||||||
d.Actions.Selection,
|
d.Actions.Selection,
|
||||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)),
|
||||||
Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager),
|
Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager),
|
||||||
Confirmations: new ConfirmationRuntimeBindings(
|
Confirmations: new ConfirmationRuntimeBindings(
|
||||||
(type, context, accepted) =>
|
(type, context, accepted) =>
|
||||||
|
|
@ -768,7 +768,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
item,
|
item,
|
||||||
inscription),
|
inscription),
|
||||||
text =>
|
text =>
|
||||||
d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)),
|
||||||
StackSplitQuantity: d.StackSplitQuantity,
|
StackSplitQuantity: d.StackSplitQuantity,
|
||||||
Plugins: d.UiRegistry,
|
Plugins: d.UiRegistry,
|
||||||
Persistence: persistence,
|
Persistence: persistence,
|
||||||
|
|
|
||||||
|
|
@ -1125,7 +1125,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.Actions.CombatMode,
|
d.Actions.CombatMode,
|
||||||
d.Log,
|
d.Log,
|
||||||
debugToast,
|
debugToast,
|
||||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au));
|
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal));
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
"live combat-mode commands",
|
"live combat-mode commands",
|
||||||
d.CombatModeCommands.BindOwned(combatCommand));
|
d.CombatModeCommands.BindOwned(combatCommand));
|
||||||
|
|
|
||||||
|
|
@ -240,11 +240,11 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
|
||||||
bindings.Chat.OnSelfSent(
|
bindings.Chat.OnSelfSent(
|
||||||
ChatKind.Tell,
|
ChatKind.Tell,
|
||||||
command.Text,
|
command.Text,
|
||||||
targetOrChannel: command.TargetName,
|
|
||||||
// Retail's own "You tell ..." echo is Speech_Direct_Send
|
// Retail's own "You tell ..." echo is Speech_Direct_Send
|
||||||
// (0x04), distinct from an incoming Tell's 0x03 — see
|
// (0x04), distinct from an incoming Tell's 0x03 — see
|
||||||
// ChatMessageType.OutgoingTell's "You tell ..." comment.
|
// ChatMessageType.OutgoingTell's "You tell ..." comment.
|
||||||
logTextType: 0x04u);
|
logTextType: (uint)RetailLogTextType.SpeechDirectSend,
|
||||||
|
targetOrChannel: command.TargetName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +290,9 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
|
||||||
targetOrChannel: legacy.Value.DisplayName,
|
targetOrChannel: legacy.Value.DisplayName,
|
||||||
// Precise per-bit own-send type (LegacyChannelChatType.Resolve's
|
// Precise per-bit own-send type (LegacyChannelChatType.Resolve's
|
||||||
// ownSend:true branch) — e.g. Fellowship keeps 0x13, Patron/
|
// 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));
|
logTextType: LegacyChannelChatType.Resolve(legacy.Value.ChannelId, ownSend: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -327,10 +327,15 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ToggleUiLock: () =>
|
ToggleUiLock: () =>
|
||||||
_interaction.Settings.SetUiLocked(
|
_interaction.Settings.SetUiLocked(
|
||||||
!_interaction.Settings.Gameplay.LockUI),
|
!_interaction.Settings.Gameplay.LockUI),
|
||||||
// Client-local text (never reaches the wire), 0x1A — same
|
// ClientCommandController's informational output sink — 0x00
|
||||||
// category as ChatVM.ShowSystemMessage.
|
// 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:
|
ShowSystemMessage:
|
||||||
text => _domain.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
text => _domain.Communication.Chat.OnSystemMessage(text, 0x00u),
|
||||||
ShowWeenieError:
|
ShowWeenieError:
|
||||||
code => _domain.Communication.Chat.OnWeenieError(code, null),
|
code => _domain.Communication.Chat.OnWeenieError(code, null),
|
||||||
PlayerPublicWeenieBitfield: () =>
|
PlayerPublicWeenieBitfield: () =>
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,14 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
||||||
// appended line — an out-of-range LogTextType leaves it unchanged
|
// appended line — an out-of-range LogTextType leaves it unchanged
|
||||||
// rather than reverting to a default (research doc §3.2). Seed the
|
// rather than reverting to a default (research doc §3.2). Seed the
|
||||||
// carry with retail's own unfilled-slot default (colorGreen, index
|
// 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);
|
RetailChatColorTable.TryGetColor(0x00u, out Vector4 currentColor);
|
||||||
var result = new List<UiText.Line>(detailed.Count);
|
var result = new List<UiText.Line>(detailed.Count);
|
||||||
foreach (var d in detailed)
|
foreach (var d in detailed)
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ public static class GameEventWiring
|
||||||
// ClientCombatSystem::HandleKillerNotificationEvent @0x0056C410
|
// ClientCombatSystem::HandleKillerNotificationEvent @0x0056C410
|
||||||
// (pc:359548-359559), which calls AddTextToScroll(..., 0, 1, 0)
|
// (pc:359548-359559), which calls AddTextToScroll(..., 0, 1, 0)
|
||||||
// — LogTextType 0x00 Default, not a combat color.
|
// — 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 =>
|
registrar.Register(GameEventType.DefenderNotification, e =>
|
||||||
{
|
{
|
||||||
|
|
@ -302,7 +302,7 @@ public static class GameEventWiring
|
||||||
{
|
{
|
||||||
var p = GameEvents.ParseKillerNotification(e.Payload.Span);
|
var p = GameEvents.ParseKillerNotification(e.Payload.Span);
|
||||||
// Same handler/type as VictimNotification above — 0x00 Default.
|
// 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 ────────────────────────────────────────────────
|
// ── Spells ────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,11 @@ public sealed class ChatLog
|
||||||
/// (<c>speech.ChatType</c>) — passed through VERBATIM, with zero
|
/// (<c>speech.ChatType</c>) — passed through VERBATIM, with zero
|
||||||
/// remapping, matching retail's <c>Handle_Communication__HearSpeech
|
/// remapping, matching retail's <c>Handle_Communication__HearSpeech
|
||||||
/// @0x005712A0</c> (the raw <c>arg5</c> feeds <c>AddTextToScroll</c>
|
/// @0x005712A0</c> (the raw <c>arg5</c> feeds <c>AddTextToScroll</c>
|
||||||
/// directly). Defaults to <c>0x02</c> (Speech) for callers that
|
/// directly). Required — no production caller relies on a default;
|
||||||
|
/// retail's normal value is <c>0x02</c> (Speech) for callers that
|
||||||
/// don't have a wire value in hand.
|
/// don't have a wire value in hand.
|
||||||
/// </param>
|
/// </param>
|
||||||
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
|
// Phase J: ACE's HandleActionTalk broadcasts a HearSpeech echo
|
||||||
// back to the sender too. Detect own echo by guid match and
|
// 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 —
|
// Retail hard-codes Emote (0x0C) for every HearEmote line —
|
||||||
// ClientCommunicationSystem::HearEmote @0x0057CBE0, the
|
// ClientCommunicationSystem::HearEmote @0x0057CBE0, the
|
||||||
// literal constant at 0x0057CF94. Not a wire value.
|
// 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
|
// HearSoulEmote tail-calls HearEmote @0x0057D096 — same
|
||||||
// hard-coded 0x0C.
|
// hard-coded 0x0C.
|
||||||
LogTextType = 0x0Cu,
|
LogTextType = (uint)RetailLogTextType.Emote,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,14 +172,13 @@ public sealed class ChatLog
|
||||||
SenderGuid: victimGuid,
|
SenderGuid: victimGuid,
|
||||||
ChannelId: killerGuid)
|
ChannelId: killerGuid)
|
||||||
{
|
{
|
||||||
// Inferred by analogy from the sibling GameEvents this opcode
|
// Direct anchor (corrected 2026-08-09, Opus review of
|
||||||
// shares a dispatch pattern with: VictimNotification (0x01AC)
|
// 172c6f9a — replaces the earlier sibling-opcode analogy):
|
||||||
// and KillerNotification (0x01AD) both route through the SAME
|
// opcode 0x019E dispatches via UIQueueManager::ProcessNet
|
||||||
// retail handler, ClientCombatSystem::HandleKillerNotification
|
// BlobData's byte table @0x55CB07 -> case 7 ->
|
||||||
// Event @0x0056C410 (cases 0xa/0xb of the combat-envelope
|
// ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320 ->
|
||||||
// switch, pc:359548-359559), which calls
|
// AddTextToScroll(..., 0, 1, 0) @0x0056C3D8. Type 0x00 Default
|
||||||
// AddTextToScroll(..., 0, 1, 0) — type 0x00 Default. No direct
|
// confirmed.
|
||||||
// decomp citation was traced for 0x019E PlayerKilled itself.
|
|
||||||
LogTextType = 0x00u,
|
LogTextType = 0x00u,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -216,8 +216,9 @@ public sealed class ChatLog
|
||||||
// Retail's HandleFailureEvent @0x00571990 dispatches per ERROR
|
// Retail's HandleFailureEvent @0x00571990 dispatches per ERROR
|
||||||
// CODE across an ~87-case switch, mostly AddTextToScroll(...,
|
// CODE across an ~87-case switch, mostly AddTextToScroll(...,
|
||||||
// 0, ...) with a scattered handful at 0x1a (client-local red).
|
// 0, ...) with a scattered handful at 0x1a (client-local red).
|
||||||
// A full per-code port is future work; 0x00 (Default) matches
|
// A full per-code port is future work (register row AP-176);
|
||||||
// the switch's majority behavior and is the safe baseline.
|
// 0x00 (Default) matches the switch's majority behavior and is
|
||||||
|
// the safe baseline.
|
||||||
LogTextType = 0x00u,
|
LogTextType = 0x00u,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -262,11 +263,11 @@ public sealed class ChatLog
|
||||||
/// <summary>GameEvent Tell (0x02BD) — whisper received.</summary>
|
/// <summary>GameEvent Tell (0x02BD) — whisper received.</summary>
|
||||||
/// <param name="logTextType">
|
/// <param name="logTextType">
|
||||||
/// The wire <c>chatType</c> from the Tell GameEvent payload
|
/// The wire <c>chatType</c> from the Tell GameEvent payload
|
||||||
/// (<c>GameEvents.Tell.ChatType</c>) — retail's normal value is
|
/// (<c>GameEvents.Tell.ChatType</c>) — required; no production caller
|
||||||
/// <c>0x03</c> (Tell), which is also this parameter's default for
|
/// relies on a default. Retail's normal value is <c>0x03</c> (Tell)
|
||||||
/// callers without a wire value in hand.
|
/// for callers without a wire value in hand.
|
||||||
/// </param>
|
/// </param>
|
||||||
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(
|
Append(new ChatEntry(
|
||||||
Kind: ChatKind.Tell,
|
Kind: ChatKind.Tell,
|
||||||
|
|
@ -352,19 +353,26 @@ public sealed class ChatLog
|
||||||
/// <c>warning().combat()</c> / <c>error().combat()</c> tag flow at
|
/// <c>warning().combat()</c> / <c>error().combat()</c> tag flow at
|
||||||
/// <c>chat.rs:221-308</c>.
|
/// <c>chat.rs:221-308</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="text">The pre-formatted combat line text.</param>
|
||||||
/// <param name="logTextType">
|
/// <param name="logTextType">
|
||||||
/// The retail <c>LogTextType</c> for this combat line. Callers should
|
/// The retail <c>LogTextType</c> for this combat line — required; no
|
||||||
/// pass one of the ACE-cited combat types (<c>0x16</c> Combat_Self for
|
/// production caller relies on a default. Callers should pass one of
|
||||||
/// lines about the local player's OWN offensive action, <c>0x15</c>
|
/// the ACE-cited combat types (<c>0x16</c> Combat_Self for lines about
|
||||||
/// Combat_Enemy for lines about an enemy's action against the local
|
/// the local player's OWN offensive action, <c>0x15</c> Combat_Enemy
|
||||||
/// player — see <see cref="CombatChatTranslator"/>) or <c>0x00</c>
|
/// for lines about an enemy's action against the local player — see
|
||||||
/// Default for retail's decompiled kill/death-notification color
|
/// <see cref="CombatChatTranslator"/>), <c>0x00</c> Default for
|
||||||
/// (<c>HandleKillerNotificationEvent @0x0056C410</c>). Defaults to
|
/// retail's decompiled kill/death-notification color
|
||||||
/// <c>0x06</c> (the generic Combat slot) for callers with no more
|
/// (<c>HandleKillerNotificationEvent @0x0056C410</c>), or <c>0x06</c>
|
||||||
/// specific classification in hand.
|
/// (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).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="kind">
|
||||||
|
/// Severity bucket for panel coloring; defaults to
|
||||||
|
/// <see cref="Combat.CombatLineKind.Info"/>.
|
||||||
/// </param>
|
/// </param>
|
||||||
public void OnCombatLine(
|
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(
|
Append(new ChatEntry(
|
||||||
Kind: ChatKind.Combat,
|
Kind: ChatKind.Combat,
|
||||||
|
|
@ -400,17 +408,16 @@ public sealed class ChatLog
|
||||||
/// (a real incoming Tell carries the sender's player guid).
|
/// (a real incoming Tell carries the sender's player guid).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="logTextType">
|
/// <param name="logTextType">
|
||||||
/// The retail <c>LogTextType</c> for this self-sent line. When
|
/// The retail <c>LogTextType</c> for this self-sent line — required;
|
||||||
/// <see langword="null"/>, defaults to <c>0x04</c> Speech_Direct_Send
|
/// no production caller relies on a default (the old null-fallback
|
||||||
/// for Tell (retail's own-echo "You tell ..." type — cross-check
|
/// ternary is retired). Pass <c>0x04</c> Speech_Direct_Send for Tell
|
||||||
/// ACE's <c>ChatMessageType.OutgoingTell</c> comment "You tell ...") or
|
/// (retail's own-echo "You tell ..." type — cross-check ACE's
|
||||||
/// <c>0x0B</c> Social_Send for Channel (the simplified own-send default
|
/// <c>ChatMessageType.OutgoingTell</c> comment "You tell ...") or, for
|
||||||
/// research doc §3.3 records; the LiveSessionCommandRouter production
|
/// Channel, the precise per-channel-bit value from
|
||||||
/// caller overrides this with the precise per-channel-bit value from
|
/// <see cref="LegacyChannelChatType.Resolve"/> — the
|
||||||
/// <see cref="LegacyChannelChatType.Resolve"/> instead of relying on
|
/// LiveSessionCommandRouter production caller does exactly this.
|
||||||
/// this fallback).
|
|
||||||
/// </param>
|
/// </param>
|
||||||
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(
|
Append(new ChatEntry(
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
|
|
@ -425,7 +432,7 @@ public sealed class ChatLog
|
||||||
ChannelId: 0)
|
ChannelId: 0)
|
||||||
{
|
{
|
||||||
ChannelName = kind == ChatKind.Channel ? targetOrChannel : "",
|
ChannelName = kind == ChatKind.Channel ? targetOrChannel : "",
|
||||||
LogTextType = logTextType ?? (kind == ChatKind.Tell ? 0x04u : 0x0Bu),
|
LogTextType = logTextType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,13 @@ public sealed class CombatChatTranslator : IDisposable
|
||||||
// grows that field, append " Critical hit." here.
|
// grows that field, append " Critical hit." here.
|
||||||
"",
|
"",
|
||||||
FormatAttackConditionsSuffix(0));
|
FormatAttackConditionsSuffix(0));
|
||||||
// Combat_Self (0x16): retail squelch-checks the attacker's OWN
|
// Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred —
|
||||||
// outgoing-hit notification against ChatMessageType.CombatSelf —
|
// ClientCombatSystem::HandleAttackerNotificationEvent @0x0056B420
|
||||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:162-163
|
// sets type 0x16 at @0x0056B761, the binary's only 0x16 producer
|
||||||
// (GameEventAttackerNotification, "You hit X...").
|
// for this notification family. ACE's ChatMessageType.CombatSelf
|
||||||
_chat.OnCombatLine(line, CombatLineKind.Info, logTextType: 0x16u);
|
// 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)
|
private void HandleDamageTaken(CombatState.DamageIncoming e)
|
||||||
|
|
@ -142,33 +144,40 @@ public sealed class CombatChatTranslator : IDisposable
|
||||||
sb.Append('.');
|
sb.Append('.');
|
||||||
if (e.Critical) sb.Append(" Critical hit.");
|
if (e.Critical) sb.Append(" Critical hit.");
|
||||||
sb.Append(FormatAttackConditionsSuffix(0));
|
sb.Append(FormatAttackConditionsSuffix(0));
|
||||||
// Combat_Enemy (0x15): retail squelch-checks the defender's
|
// Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred —
|
||||||
// incoming-hit notification against ChatMessageType.CombatEnemy —
|
// ClientCombatSystem::HandleDefenderNotificationEvent @0x0056C920
|
||||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:541
|
// sets type 0x15 at @0x0056D4B4, the binary's only 0x15 producer
|
||||||
// (GameEventDefenderNotification, "X hit you...").
|
// for this notification family. ACE's ChatMessageType.CombatEnemy
|
||||||
_chat.OnCombatLine(sb.ToString(), CombatLineKind.Warning, logTextType: 0x15u);
|
// 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)
|
private void HandleMissedOutgoing(string defenderName)
|
||||||
{
|
{
|
||||||
// chat.rs:286-291 — EvasionAttackerNotification:
|
// chat.rs:286-291 — EvasionAttackerNotification:
|
||||||
// "{} evaded your attack."
|
// "{} evaded your attack."
|
||||||
// Combat_Self (0x16): this is about the LOCAL PLAYER'S OWN attack
|
// Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred —
|
||||||
// missing — retail squelch-checks GameEventEvasionAttackerNotification
|
// ClientCombatSystem::HandleEvasionAttackerNotificationEvent
|
||||||
// against CombatSelf, same family as the hit-dealt line above —
|
// @0x0056C7A0 sets type 0x16 at @0x0056C870, the binary's only
|
||||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:150.
|
// 0x16 producer for this notification family (same slot as the
|
||||||
_chat.OnCombatLine($"{defenderName} evaded your attack.", CombatLineKind.Info, logTextType: 0x16u);
|
// 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)
|
private void HandleEvadedIncoming(string attackerName)
|
||||||
{
|
{
|
||||||
// chat.rs:292-297 — EvasionDefenderNotification:
|
// chat.rs:292-297 — EvasionDefenderNotification:
|
||||||
// "You evaded {}'s attack."
|
// "You evaded {}'s attack."
|
||||||
// Combat_Enemy (0x15): this is about an ENEMY'S attack (that the
|
// Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred —
|
||||||
// local player evaded) — retail squelch-checks
|
// ClientCombatSystem::HandleEvasionDefenderNotificationEvent
|
||||||
// GameEventEvasionDefenderNotification against CombatEnemy —
|
// @0x0056C620 sets type 0x15 at @0x0056C710, the binary's only
|
||||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:345.
|
// 0x15 producer for this notification family. ACE's
|
||||||
_chat.OnCombatLine($"You evaded {attackerName}'s attack.", CombatLineKind.Info, logTextType: 0x15u);
|
// 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)
|
private void HandleKillLanded(string victimName, uint victimGuid)
|
||||||
|
|
@ -185,7 +194,7 @@ public sealed class CombatChatTranslator : IDisposable
|
||||||
// both via ClientCombatSystem::HandleKillerNotificationEvent
|
// both via ClientCombatSystem::HandleKillerNotificationEvent
|
||||||
// @0x0056C410) calls AddTextToScroll(..., 0, 1, 0) — see
|
// @0x0056C410) calls AddTextToScroll(..., 0, 1, 0) — see
|
||||||
// ChatLog.OnPlayerKilled's identical citation.
|
// 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) ───────────────────
|
// ── Formatters (ported VERBATIM from chat.rs:561-595) ───────────────────
|
||||||
|
|
|
||||||
|
|
@ -16,62 +16,94 @@ namespace AcDream.Core.Chat;
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
/// <b>Opus review correction, 2026-08-09.</b> The catch-all and two named
|
||||||
|
/// cases below were WRONG in the original CH1 port. Binary Ninja renders
|
||||||
|
/// retail's <c>neg esi; sbb esi, esi</c> idiom — a branchless "esi = (cond)
|
||||||
|
/// ? -1 : 0" pattern used to pick between <c>Channel</c> (0x08) and
|
||||||
|
/// <c>Channel_Send</c> (0x09) — as the pseudo-C expression <c>esi - esi</c>,
|
||||||
|
/// which trivially evaluates to <c>0</c> and hides the real values. The
|
||||||
|
/// correction was made by decoding the raw bytes at the PDB-paired binary:
|
||||||
|
/// the HEAR branch's <c>sbb</c> site is at VA <c>0x00570F0A</c> (mask
|
||||||
|
/// <c>-6</c> maps to <c>0x08</c> Channel), and the SEND branch's is at VA
|
||||||
|
/// <c>0x00570D4F</c> (mask <c>-5</c> maps to <c>0x09</c> Channel_Send).
|
||||||
|
/// ACE's PDB-sourced <c>Channel</c> enum corroborates: Abuse (bit
|
||||||
|
/// <c>0x0001</c>) is retail's ONLY producer of <c>0x0E</c>, Help (bit
|
||||||
|
/// <c>0x0400</c>) is the only producer of <c>0x0F</c>, and every other
|
||||||
|
/// unmatched bit (the admin/audit/advocate/QA/sentinel/town family) resolves
|
||||||
|
/// to the plain <c>Channel</c>/<c>Channel_Send</c> pair, not Abuse's 0x0E.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
/// Per-bit findings from the decompiled dispatch (not all bits behave
|
/// Per-bit findings from the decompiled dispatch (not all bits behave
|
||||||
/// the same for own-send — only Patron/Vassal/Follower get a distinct
|
/// the same for own-send — only Patron/Vassal/Follower and the
|
||||||
/// send-color; Fellowship, Co-Vassals, Allegiance Broadcast, and the
|
/// FellowBroadcast bit get a distinct send-color; Fellowship, Co-Vassals,
|
||||||
/// generic/admin catch-all use the SAME type for hear and send):
|
/// and Allegiance Broadcast use the SAME type for hear and send):
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class LegacyChannelChatType
|
public static class LegacyChannelChatType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve <paramref name="channelBit"/> (the <c>ChannelBroadcast</c>
|
/// Resolve <paramref name="channelId"/> (the <c>ChannelBroadcast</c>
|
||||||
/// wire channel id, a single flag bit) to a <c>LogTextType</c>.
|
/// wire channel id) to a <c>LogTextType</c>. <paramref name="channelId"/>
|
||||||
|
/// is compared with <c>==</c> 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 <c>Channel.AllBroadcast</c> = <c>0x401</c>, which spans
|
||||||
|
/// both the Help bit <c>0x400</c> and the Abuse bit <c>0x1</c>) does not
|
||||||
|
/// equal any single named arm and falls through to the generic
|
||||||
|
/// catch-all rather than resolving to either bit's own type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channelBit">The wire channel id / bitflag.</param>
|
/// <param name="channelId">The wire channel id / bitflag.</param>
|
||||||
/// <param name="ownSend">
|
/// <param name="ownSend">
|
||||||
/// <see langword="true"/> when this is the local player's own outgoing
|
/// <see langword="true"/> when this is the local player's own outgoing
|
||||||
/// message (retail's <c>m_buffer->m_len == 1</c> self-sentinel
|
/// message (retail's <c>m_buffer->m_len == 1</c> self-sentinel
|
||||||
/// branch); <see langword="false"/> when hearing another sender.
|
/// branch); <see langword="false"/> when hearing another sender.
|
||||||
/// </param>
|
/// </param>
|
||||||
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.
|
// Fellowship: same type ("[Fellowship] ...") for hear and send.
|
||||||
// pc:00570e48 (hear, m_buffer_5=0x13) / pc:00570d08 (send, 0x13).
|
// pc:00570e48 (hear, m_buffer_5=0x13) / pc:00570d08 (send, 0x13).
|
||||||
0x0800u => 0x13u,
|
0x0800u => (uint)RetailLogTextType.Fellowship,
|
||||||
|
|
||||||
// Patron / Vassal / Follower(Monarch): hear = Social (0xA,
|
// Patron / Vassal / Follower(Monarch): hear = Social (0xA,
|
||||||
// "Your patron/vassal/follower X tells you..."); OWN send =
|
// "Your patron/vassal/follower X tells you..."); OWN send = Social_
|
||||||
// Social_Send (0xB, "You say to your patron/vassal/follower...").
|
// Send (0xB, "You say to your patron/vassal/follower...").
|
||||||
// pc:00570e50/00570e58 (Patron/Vassal hear, 0xa) + pc:00570e40
|
// pc:00570e50/00570e58 (Patron/Vassal hear, 0xa) + pc:00570e40
|
||||||
// (Follower hear, 0xa); pc:00570c07/00570c21 (all three own-send
|
// (Follower hear, 0xa); pc:00570c07/00570c21 (all three own-send
|
||||||
// via the shared label_570c21, 0xb).
|
// via the shared label_570c21, 0xb).
|
||||||
0x1000u => ownSend ? 0x0Bu : 0x0Au, // Patron
|
0x1000u or 0x2000u or 0x4000u =>
|
||||||
0x2000u => ownSend ? 0x0Bu : 0x0Au, // Vassal
|
ownSend ? (uint)RetailLogTextType.SocialSend : (uint)RetailLogTextType.Social,
|
||||||
0x4000u => ownSend ? 0x0Bu : 0x0Au, // Follower / Monarch
|
|
||||||
|
|
||||||
// Co-Vassals / Allegiance Broadcast: same type for hear and send.
|
// Co-Vassals / Allegiance Broadcast: same type for hear and send.
|
||||||
// pc:00571025/00571014 (hear, 0xa) / pc:00570e17/00570df7 (send, 0xa).
|
// pc:00571025/00571014 (hear, 0xa) / pc:00570e17/00570df7 (send, 0xa).
|
||||||
0x1000000u => 0x0Au, // Co-Vassals
|
0x1000000u or 0x2000000u => (uint)RetailLogTextType.Social,
|
||||||
0x2000000u => 0x0Au, // Allegiance Broadcast
|
|
||||||
|
|
||||||
// Unnamed bit — no producer traced, but retail's own dispatch
|
// FellowBroadcast: the sbb idiom's ebp==0x4000000 special case picks
|
||||||
// assigns it 0x13 (Fellowship's slot) for both hear and send.
|
// Fellowship's OWN-SEND slot (0x13) but the plain sbb Channel value
|
||||||
// pc:00570d43 (send, 0x13); the hear branch mirrors it via the
|
// (0x08) for hear — pc:00570d43 (send, 0x13). This is the one
|
||||||
// same ebp==0x4000000 special case one level up in the dispatch.
|
// unnamed bit that behaves like neither a pure catch-all nor a pure
|
||||||
0x4000000u => 0x13u,
|
// Fellowship alias.
|
||||||
|
0x4000000u =>
|
||||||
|
ownSend ? (uint)RetailLogTextType.Fellowship : (uint)RetailLogTextType.Channel,
|
||||||
|
|
||||||
// The single named non-family bit inside the generic catch-all:
|
// Generic catch-all: the sbb-derived Channel (hear, 0x08) /
|
||||||
// 0x400 gets its OWN color (Help, 0xF) where every other
|
// Channel_Send (own-send, 0x09) pair — Admin, Audit, Advocate, QA,
|
||||||
// unmatched bit gets the catch-all (Abuse, 0xE). Both branches
|
// Sentinel, town channels, and every other unmatched bit. This is
|
||||||
// (hear label_570f0a and send label_570d4f) test `ebp != 0x400`
|
// the branch Binary Ninja's `esi - esi` mis-rendering hid; the real
|
||||||
// with the identical 0xe/0xf split.
|
// masks are decoded from the raw bytes at VA 0x00570F0A (hear,
|
||||||
0x0400u => 0x0Fu,
|
// mask -6 -> 0x08) and VA 0x00570D4F (send, mask -5 -> 0x09).
|
||||||
|
_ => ownSend ? (uint)RetailLogTextType.ChannelSend : (uint)RetailLogTextType.Channel,
|
||||||
// Generic/admin/audit/sentinel catch-all: retail's
|
|
||||||
// "<Tell> 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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
src/AcDream.Core/Chat/RetailLogTextType.cs
Normal file
87
src/AcDream.Core/Chat/RetailLogTextType.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
namespace AcDream.Core.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The 34-value retail wire <c>LogTextType</c> index space (the same integer
|
||||||
|
/// ACE calls <c>ChatMessageType</c>). Values only — no color, no presentation
|
||||||
|
/// data; <c>AcDream.Core</c> stays presentation-free per Code Structure Rule 2.
|
||||||
|
/// The actual color lookup lives in
|
||||||
|
/// <c>AcDream.UI.Abstractions.Panels.Chat.RetailChatColorTable</c>, which is
|
||||||
|
/// keyed by these same numeric values.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Names <c>0x00</c>-<c>0x1F</c> are retail's own strings from
|
||||||
|
/// <c>LogTextTypeEnumMapper::LogTextTypeToString @0x006AFF90</c> (a literal
|
||||||
|
/// switch over the wire byte). Names for <c>0x1A</c>, <c>0x1B</c>-<c>0x1E</c>,
|
||||||
|
/// <c>0x20</c>, and <c>0x21</c> 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
|
||||||
|
/// <c>docs/research/2026-08-09-chat-retail-color-table.md</c> §2.1-2.2 for the
|
||||||
|
/// full derivation and per-slot evidence).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public enum RetailLogTextType : uint
|
||||||
|
{
|
||||||
|
/// <summary>Default-fill slot; retail's own unfilled-slot default.</summary>
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// acdream label — retail's own mapper has no string for this slot.
|
||||||
|
/// Client-local text that never reaches the wire: command-parser
|
||||||
|
/// errors, the <c>cant_jump_*</c> family, most informational command
|
||||||
|
/// output. See <c>docs/research/2026-08-09-chat-retail-color-table.md</c>
|
||||||
|
/// §2.2.
|
||||||
|
/// </summary>
|
||||||
|
ClientLocal = 0x1A,
|
||||||
|
|
||||||
|
/// <summary>acdream label — the Turbine "General" community room.</summary>
|
||||||
|
TurbineGeneral = 0x1B,
|
||||||
|
|
||||||
|
/// <summary>acdream label — the Turbine "Trade" community room.</summary>
|
||||||
|
TurbineTrade = 0x1C,
|
||||||
|
|
||||||
|
/// <summary>acdream label — the Turbine "LFG" community room.</summary>
|
||||||
|
TurbineLFG = 0x1D,
|
||||||
|
|
||||||
|
/// <summary>acdream label — the Turbine "Roleplay" community room.</summary>
|
||||||
|
TurbineRoleplay = 0x1E,
|
||||||
|
|
||||||
|
AdminTell = 0x1F,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// acdream label — the Turbine "Society" community rooms (all four
|
||||||
|
/// society sub-rooms share this one slot in retail).
|
||||||
|
/// </summary>
|
||||||
|
TurbineSociety = 0x20,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
Reserved21 = 0x21,
|
||||||
|
}
|
||||||
|
|
@ -223,12 +223,17 @@ internal sealed class HeadlessGameplayOperations
|
||||||
session!.SendCastTargetedSpell(targetId, spellId);
|
session!.SendCastTargetedSpell(targetId, spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-local text (a bot script's own injected message, never
|
// Informational sink — 0x00 Default, NOT 0x1A (corrected 2026-08-09,
|
||||||
// reaches the wire), 0x1A — same category as ChatVM.ShowSystemMessage.
|
// 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) =>
|
public void DisplayMessage(string message) =>
|
||||||
RequireRuntime().CommunicationOwner.Chat.OnSystemMessage(
|
RequireRuntime().CommunicationOwner.Chat.OnSystemMessage(
|
||||||
message,
|
message,
|
||||||
chatType: 0x1Au);
|
chatType: 0x00u);
|
||||||
|
|
||||||
public void IncrementBusy() =>
|
public void IncrementBusy() =>
|
||||||
RequireRuntime().ActionOwner.Transactions
|
RequireRuntime().ActionOwner.Transactions
|
||||||
|
|
|
||||||
|
|
@ -113,12 +113,16 @@ public sealed class ChatVM : IDisposable
|
||||||
/// local feedback without round-tripping the server.
|
/// local feedback without round-tripping the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// LogTextType <c>0x1A</c>: this text never reaches the wire — the
|
/// LogTextType <c>0x00</c> Default, NOT <c>0x1A</c> (corrected 2026-08-09,
|
||||||
/// same "client-local text" category as retail's own command-parser
|
/// Opus review of 172c6f9a). This sink is <c>ClientCommandController</c>'s
|
||||||
/// errors and <c>cant_jump_*</c> strings (research doc
|
/// general-purpose output — @version, /loc, friends list, usage lines —
|
||||||
/// <c>docs/research/2026-08-09-chat-retail-color-table.md</c> §2.2).
|
/// and retail types the great majority of that informational command
|
||||||
|
/// output <c>0x00</c>, reserving <c>0x1A</c> (bright red) for genuine
|
||||||
|
/// refusals/errors. The refusal-vs-informational split lands with CH2's
|
||||||
|
/// producer rewiring (SpewBox routing) — see
|
||||||
|
/// <c>docs/research/2026-08-09-chat-retail-interface-text.md</c> §7.2.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x1Au);
|
public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x00u);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Drain the chat log. Used by the /clear client-side command.
|
/// Drain the chat log. Used by the /clear client-side command.
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ public class ChatWindowControllerTests
|
||||||
/// send (Type-3) [0x10000019]
|
/// send (Type-3) [0x10000019]
|
||||||
/// maxmin (Type-3) [0x1000046F]
|
/// maxmin (Type-3) [0x1000046F]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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
|
var transcriptNode = new ElementInfo
|
||||||
{
|
{
|
||||||
|
|
@ -116,7 +117,7 @@ public class ChatWindowControllerTests
|
||||||
root.Children.Add(maxMinNode);
|
root.Children.Add(maxMinNode);
|
||||||
|
|
||||||
var layout = LayoutImporter.Build(root, NoTex, null);
|
var layout = LayoutImporter.Build(root, NoTex, null);
|
||||||
var vm = new ChatVM(new ChatLog());
|
var vm = new ChatVM(log ?? new ChatLog());
|
||||||
return (root, layout, vm);
|
return (root, layout, vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,6 +210,34 @@ public class ChatWindowControllerTests
|
||||||
Assert.Equal(4, ctrl.TranscriptLayoutBuildCount);
|
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<UiText.Line> 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 ─────
|
// ── Test 4: Input.OnSubmit publishes SendChatCmd via the capture bus ─────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,10 @@ public sealed class GameEventWiringTests
|
||||||
Assert.Equal(ChatKind.Channel, entry.Kind);
|
Assert.Equal(ChatKind.Channel, entry.Kind);
|
||||||
Assert.Equal("Alice", entry.Sender);
|
Assert.Equal("Alice", entry.Sender);
|
||||||
// channelId 42 (0x2A) matches no named legacy bit — generic
|
// channelId 42 (0x2A) matches no named legacy bit — generic
|
||||||
// admin/audit catch-all (LegacyChannelChatType.Resolve → 0x0E).
|
// admin/audit catch-all, hear branch (LegacyChannelChatType.Resolve
|
||||||
Assert.Equal(0x0Eu, entry.LogTextType);
|
// → 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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ public sealed class ChatCommandTargetStateTests
|
||||||
var chat = new ChatLog();
|
var chat = new ChatLog();
|
||||||
using var targets = new ChatCommandTargetState(chat);
|
using var targets = new ChatCommandTargetState(chat);
|
||||||
|
|
||||||
chat.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
chat.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u);
|
||||||
chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith");
|
||||||
|
|
||||||
Assert.Equal("Bestie", targets.LastIncomingTellSender);
|
Assert.Equal("Bestie", targets.LastIncomingTellSender);
|
||||||
Assert.Equal("Caith", targets.LastOutgoingTellTarget);
|
Assert.Equal("Caith", targets.LastOutgoingTellTarget);
|
||||||
|
|
@ -22,8 +22,8 @@ public sealed class ChatCommandTargetStateTests
|
||||||
{
|
{
|
||||||
var chat = new ChatLog();
|
var chat = new ChatLog();
|
||||||
using var targets = new ChatCommandTargetState(chat);
|
using var targets = new ChatCommandTargetState(chat);
|
||||||
chat.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
chat.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u);
|
||||||
chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith");
|
||||||
|
|
||||||
targets.ResetSession();
|
targets.ResetSession();
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ public sealed class ChatCommandTargetStateTests
|
||||||
|
|
||||||
targets.Dispose();
|
targets.Dispose();
|
||||||
targets.Dispose();
|
targets.Dispose();
|
||||||
chat.OnTellReceived("After", "ignored", 0x50000002u);
|
chat.OnTellReceived("After", "ignored", 0x50000002u, logTextType: 0x03u);
|
||||||
|
|
||||||
Assert.Null(targets.LastIncomingTellSender);
|
Assert.Null(targets.LastIncomingTellSender);
|
||||||
Assert.Null(targets.LastOutgoingTellTarget);
|
Assert.Null(targets.LastOutgoingTellTarget);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public sealed class ChatLogLocalGuidTests
|
||||||
log.SetLocalPlayerGuid(0x5000_000A);
|
log.SetLocalPlayerGuid(0x5000_000A);
|
||||||
|
|
||||||
log.OnLocalSpeech("+Acdream", "hello world",
|
log.OnLocalSpeech("+Acdream", "hello world",
|
||||||
senderGuid: 0x5000_000A, isRanged: false);
|
senderGuid: 0x5000_000A, isRanged: false, logTextType: 0x02u);
|
||||||
|
|
||||||
var entry = log.Snapshot()[0];
|
var entry = log.Snapshot()[0];
|
||||||
Assert.Equal(ChatKind.LocalSpeech, entry.Kind);
|
Assert.Equal(ChatKind.LocalSpeech, entry.Kind);
|
||||||
|
|
@ -35,7 +35,7 @@ public sealed class ChatLogLocalGuidTests
|
||||||
log.SetLocalPlayerGuid(0x5000_000A);
|
log.SetLocalPlayerGuid(0x5000_000A);
|
||||||
|
|
||||||
log.OnLocalSpeech("Caith", "hi",
|
log.OnLocalSpeech("Caith", "hi",
|
||||||
senderGuid: 0x5000_0042, isRanged: false);
|
senderGuid: 0x5000_0042, isRanged: false, logTextType: 0x02u);
|
||||||
|
|
||||||
Assert.Equal("Caith", log.Snapshot()[0].Sender);
|
Assert.Equal("Caith", log.Snapshot()[0].Sender);
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ public sealed class ChatLogLocalGuidTests
|
||||||
// arrive with sender="" before the player has a guid.
|
// arrive with sender="" before the player has a guid.
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnLocalSpeech("", "anyone home?",
|
log.OnLocalSpeech("", "anyone home?",
|
||||||
senderGuid: 0u, isRanged: true);
|
senderGuid: 0u, isRanged: true, logTextType: 0x02u);
|
||||||
|
|
||||||
Assert.Equal("You", log.Snapshot()[0].Sender);
|
Assert.Equal("You", log.Snapshot()[0].Sender);
|
||||||
Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind);
|
Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind);
|
||||||
|
|
@ -62,13 +62,13 @@ public sealed class ChatLogLocalGuidTests
|
||||||
const uint newGuid = 0x50000002u;
|
const uint newGuid = 0x50000002u;
|
||||||
log.SetLocalPlayerGuid(oldGuid);
|
log.SetLocalPlayerGuid(oldGuid);
|
||||||
log.OnSystemMessage("session boundary", 1u);
|
log.OnSystemMessage("session boundary", 1u);
|
||||||
log.OnLocalSpeech("Old", "before", oldGuid, isRanged: false);
|
log.OnLocalSpeech("Old", "before", oldGuid, isRanged: false, logTextType: 0x02u);
|
||||||
|
|
||||||
log.ResetSessionIdentity();
|
log.ResetSessionIdentity();
|
||||||
log.OnSystemMessage("session boundary", 1u);
|
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.SetLocalPlayerGuid(newGuid);
|
||||||
log.OnLocalSpeech("New", "new", newGuid, isRanged: false);
|
log.OnLocalSpeech("New", "new", newGuid, isRanged: false, logTextType: 0x02u);
|
||||||
|
|
||||||
ChatEntry[] entries = log.Snapshot();
|
ChatEntry[] entries = log.Snapshot();
|
||||||
Assert.Equal(5, entries.Length);
|
Assert.Equal(5, entries.Length);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public sealed class ChatLogTests
|
||||||
ChatEntry? seen = null;
|
ChatEntry? seen = null;
|
||||||
log.EntryAppended += e => seen = e;
|
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.Equal(1, log.Count);
|
||||||
Assert.NotNull(seen);
|
Assert.NotNull(seen);
|
||||||
|
|
@ -26,7 +26,7 @@ public sealed class ChatLogTests
|
||||||
public void OnLocalSpeech_Ranged_SetsRangedKind()
|
public void OnLocalSpeech_Ranged_SetsRangedKind()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ public sealed class ChatLogTests
|
||||||
public void OnTellReceived_SetsTellKind()
|
public void OnTellReceived_SetsTellKind()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnTellReceived("Alice", "psst", 0xAA);
|
log.OnTellReceived("Alice", "psst", 0xAA, logTextType: 0x03u);
|
||||||
Assert.Equal(ChatKind.Tell, log.Snapshot()[0].Kind);
|
Assert.Equal(ChatKind.Tell, log.Snapshot()[0].Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ public sealed class ChatLogTests
|
||||||
public void OnSelfSent_EchoesOutbound()
|
public void OnSelfSent_EchoesOutbound()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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];
|
var e = log.Snapshot()[0];
|
||||||
Assert.Equal("Alice", e.Sender);
|
Assert.Equal("Alice", e.Sender);
|
||||||
Assert.Equal("hey", e.Text);
|
Assert.Equal("hey", e.Text);
|
||||||
|
|
@ -72,10 +72,10 @@ public sealed class ChatLogTests
|
||||||
public void RingBuffer_DropsOldestBeyondCapacity()
|
public void RingBuffer_DropsOldestBeyondCapacity()
|
||||||
{
|
{
|
||||||
var log = new ChatLog(maxEntries: 3);
|
var log = new ChatLog(maxEntries: 3);
|
||||||
log.OnLocalSpeech("A", "1", 0, false);
|
log.OnLocalSpeech("A", "1", 0, false, logTextType: 0x02u);
|
||||||
log.OnLocalSpeech("B", "2", 0, false);
|
log.OnLocalSpeech("B", "2", 0, false, logTextType: 0x02u);
|
||||||
log.OnLocalSpeech("C", "3", 0, false);
|
log.OnLocalSpeech("C", "3", 0, false, logTextType: 0x02u);
|
||||||
log.OnLocalSpeech("D", "4", 0, false);
|
log.OnLocalSpeech("D", "4", 0, false, logTextType: 0x02u);
|
||||||
|
|
||||||
var snap = log.Snapshot();
|
var snap = log.Snapshot();
|
||||||
Assert.Equal(3, snap.Length);
|
Assert.Equal(3, snap.Length);
|
||||||
|
|
@ -87,7 +87,7 @@ public sealed class ChatLogTests
|
||||||
public void Clear_EmptiesBuffer()
|
public void Clear_EmptiesBuffer()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnLocalSpeech("A", "1", 0, false);
|
log.OnLocalSpeech("A", "1", 0, false, logTextType: 0x02u);
|
||||||
log.Clear();
|
log.Clear();
|
||||||
Assert.Equal(0, log.Count);
|
Assert.Equal(0, log.Count);
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ public sealed class ChatLogTests
|
||||||
// ranged shout). Substitute "You" so the chat line reads
|
// ranged shout). Substitute "You" so the chat line reads
|
||||||
// "You: hello" instead of ": hello".
|
// "You: hello" instead of ": hello".
|
||||||
var log = new ChatLog();
|
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];
|
var e = log.Snapshot()[0];
|
||||||
Assert.Equal("You", e.Sender);
|
Assert.Equal("You", e.Sender);
|
||||||
Assert.Equal("hello", e.Text);
|
Assert.Equal("hello", e.Text);
|
||||||
|
|
@ -182,7 +182,7 @@ public sealed class ChatLogTests
|
||||||
public void OnLocalSpeech_NonEmptySender_KeepsAsIs()
|
public void OnLocalSpeech_NonEmptySender_KeepsAsIs()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal("Alice", log.Snapshot()[0].Sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +192,7 @@ public sealed class ChatLogTests
|
||||||
public void OnCombatLine_DefaultsInfoKind_TagsEntryAsCombat()
|
public void OnCombatLine_DefaultsInfoKind_TagsEntryAsCombat()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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];
|
var e = log.Snapshot()[0];
|
||||||
Assert.Equal(ChatKind.Combat, e.Kind);
|
Assert.Equal(ChatKind.Combat, e.Kind);
|
||||||
Assert.Equal(CombatLineKind.Info, e.CombatKind);
|
Assert.Equal(CombatLineKind.Info, e.CombatKind);
|
||||||
|
|
@ -204,21 +204,21 @@ public sealed class ChatLogTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnCombatLine("Mosswart hit you for 8 fire damage to your chest.",
|
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);
|
Assert.Equal(CombatLineKind.Warning, log.Snapshot()[0].CombatKind);
|
||||||
|
|
||||||
log.OnCombatLine("Attack sequence finished with WeenieError 0x1234.",
|
log.OnCombatLine("Attack sequence finished with WeenieError 0x1234.",
|
||||||
CombatLineKind.Error);
|
logTextType: 0x06u, kind: CombatLineKind.Error);
|
||||||
Assert.Equal(CombatLineKind.Error, log.Snapshot()[1].CombatKind);
|
Assert.Equal(CombatLineKind.Error, log.Snapshot()[1].CombatKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Campaign CH slice CH1: LogTextType ingestion-site mapping ──────────
|
// ── Campaign CH slice CH1: LogTextType ingestion-site mapping ──────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnLocalSpeech_DefaultsLogTextType_ToSpeech()
|
public void OnLocalSpeech_ExplicitLogTextType_PinsSpeechValue()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(0x02u, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,10 +275,10 @@ public sealed class ChatLogTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnTellReceived_DefaultsLogTextType_ToTell()
|
public void OnTellReceived_ExplicitLogTextType_PinsTellValue()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnTellReceived("Alice", "psst", 0xAA);
|
log.OnTellReceived("Alice", "psst", 0xAA, logTextType: 0x03u);
|
||||||
Assert.Equal(0x03u, log.Snapshot()[0].LogTextType);
|
Assert.Equal(0x03u, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,18 +291,18 @@ public sealed class ChatLogTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnSelfSent_Tell_DefaultsLogTextType_ToSpeechDirectSend()
|
public void OnSelfSent_Tell_ExplicitLogTextType_PinsSpeechDirectSendValue()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(0x04u, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnSelfSent_Channel_DefaultsLogTextType_ToSocialSend()
|
public void OnSelfSent_Channel_ExplicitLogTextType_PinsSocialSendValue()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(0x0Bu, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,7 +310,7 @@ public sealed class ChatLogTests
|
||||||
public void OnSelfSent_ExplicitLogTextType_Overrides()
|
public void OnSelfSent_ExplicitLogTextType_Overrides()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(0x13u, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,12 +324,15 @@ public sealed class ChatLogTests
|
||||||
// Co-Vassals / Allegiance Broadcast.
|
// Co-Vassals / Allegiance Broadcast.
|
||||||
[InlineData(0x1000000u, "Co-Vassals", 0x0Au)]
|
[InlineData(0x1000000u, "Co-Vassals", 0x0Au)]
|
||||||
[InlineData(0x2000000u, "Allegiance Broadcast", 0x0Au)]
|
[InlineData(0x2000000u, "Allegiance Broadcast", 0x0Au)]
|
||||||
// Unnamed bit reuses Fellowship's slot.
|
// FellowBroadcast — hear is the plain Channel slot (corrected
|
||||||
[InlineData(0x4000000u, "?", 0x13u)]
|
// 2026-08-09, Opus review of 172c6f9a — was wrongly 0x13).
|
||||||
|
[InlineData(0x4000000u, "?", 0x08u)]
|
||||||
// The one named non-family bit inside the generic bucket (Help).
|
// The one named non-family bit inside the generic bucket (Help).
|
||||||
[InlineData(0x0400u, "Help", 0x0Fu)]
|
[InlineData(0x0400u, "Help", 0x0Fu)]
|
||||||
// Generic admin/audit/sentinel catch-all.
|
// Generic admin/audit/sentinel catch-all — Channel (hear), NOT Abuse
|
||||||
[InlineData(0x0900u, "Audit", 0x0Eu)]
|
// (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(
|
public void OnChannelBroadcast_DerivesLogTextType_FromLegacyChannelBit(
|
||||||
uint channelBit, string channelName, uint expectedLogTextType)
|
uint channelBit, string channelName, uint expectedLogTextType)
|
||||||
{
|
{
|
||||||
|
|
@ -359,10 +362,10 @@ public sealed class ChatLogTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnCombatLine_DefaultLogTextType_IsGenericCombat()
|
public void OnCombatLine_ExplicitLogTextType_PinsGenericCombatValue()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
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);
|
Assert.Equal(0x06u, log.Snapshot()[0].LogTextType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,15 @@ namespace AcDream.Core.Tests.Chat;
|
||||||
/// <c>ClientCommunicationSystem::Handle_Communication__ChannelBroadcast
|
/// <c>ClientCommunicationSystem::Handle_Communication__ChannelBroadcast
|
||||||
/// @0x00570B90</c> — both the HEAR (someone else's message) and OWN-SEND
|
/// @0x00570B90</c> — both the HEAR (someone else's message) and OWN-SEND
|
||||||
/// (the local player's own outgoing message) branches, which diverge for
|
/// (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.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Opus review, 2026-08-09: the catch-all and the Abuse/Help/FellowBroadcast
|
||||||
|
/// cases were corrected after decoding the raw <c>sbb</c>-idiom bytes at VA
|
||||||
|
/// <c>0x00570F0A</c> (hear) / <c>0x00570D4F</c> (send) — Binary Ninja's
|
||||||
|
/// pseudo-C had rendered the idiom as the trivial <c>esi - esi</c> (always
|
||||||
|
/// 0), hiding the real Channel (0x08) / Channel_Send (0x09) values.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LegacyChannelChatTypeTests
|
public sealed class LegacyChannelChatTypeTests
|
||||||
{
|
{
|
||||||
|
|
@ -17,23 +25,51 @@ public sealed class LegacyChannelChatTypeTests
|
||||||
[InlineData(0x0800u, 0x13u)] // Fellowship
|
[InlineData(0x0800u, 0x13u)] // Fellowship
|
||||||
[InlineData(0x1000000u, 0x0Au)] // Co-Vassals
|
[InlineData(0x1000000u, 0x0Au)] // Co-Vassals
|
||||||
[InlineData(0x2000000u, 0x0Au)] // Allegiance Broadcast
|
[InlineData(0x2000000u, 0x0Au)] // Allegiance Broadcast
|
||||||
[InlineData(0x4000000u, 0x13u)] // unnamed bit
|
public void Resolve_SameForHearAndSend(uint channelId, uint expected)
|
||||||
[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)
|
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, LegacyChannelChatType.Resolve(channelBit, ownSend: false));
|
Assert.Equal(expected, LegacyChannelChatType.Resolve(channelId, ownSend: false));
|
||||||
Assert.Equal(expected, LegacyChannelChatType.Resolve(channelBit, ownSend: true));
|
Assert.Equal(expected, LegacyChannelChatType.Resolve(channelId, ownSend: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0x1000u)] // Patron
|
[InlineData(0x1000u)] // Patron
|
||||||
[InlineData(0x2000u)] // Vassal
|
[InlineData(0x2000u)] // Vassal
|
||||||
[InlineData(0x4000u)] // Follower / Monarch
|
[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(0x0Au, LegacyChannelChatType.Resolve(channelId, ownSend: false));
|
||||||
Assert.Equal(0x0Bu, LegacyChannelChatType.Resolve(channelBit, ownSend: true));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ public sealed class RuntimeCommunicationStateTests
|
||||||
var observer = new RecordingObserver();
|
var observer = new RecordingObserver();
|
||||||
using IDisposable subscription = state.Events.Subscribe(observer);
|
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);
|
RuntimeCommunicationEvent delta = Assert.Single(observer.Events);
|
||||||
Assert.Equal(1UL, delta.Sequence);
|
Assert.Equal(1UL, delta.Sequence);
|
||||||
|
|
@ -108,8 +108,8 @@ public sealed class RuntimeCommunicationStateTests
|
||||||
public void SessionResetsClearScopedStateWithoutClearingTranscript()
|
public void SessionResetsClearScopedStateWithoutClearingTranscript()
|
||||||
{
|
{
|
||||||
using var state = new RuntimeCommunicationState();
|
using var state = new RuntimeCommunicationState();
|
||||||
state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u);
|
state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u, logTextType: 0x03u);
|
||||||
state.Chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
state.Chat.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith");
|
||||||
state.TurbineChat.OnChannelsReceived(
|
state.TurbineChat.OnChannelsReceived(
|
||||||
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
||||||
state.Friends.Apply(new FriendsUpdate(
|
state.Friends.Apply(new FriendsUpdate(
|
||||||
|
|
@ -159,7 +159,7 @@ public sealed class RuntimeCommunicationStateTests
|
||||||
using var first = new RuntimeCommunicationState();
|
using var first = new RuntimeCommunicationState();
|
||||||
using var second = 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(1, first.View.Count);
|
||||||
Assert.Equal(0, second.View.Count);
|
Assert.Equal(0, second.View.Count);
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,13 @@ public sealed class RuntimeGameplayOwnershipTests
|
||||||
communication.Chat.OnTellReceived(
|
communication.Chat.OnTellReceived(
|
||||||
"Sender",
|
"Sender",
|
||||||
"hello",
|
"hello",
|
||||||
0x50000001u);
|
0x50000001u,
|
||||||
|
logTextType: 0x03u);
|
||||||
communication.Chat.OnSelfSent(
|
communication.Chat.OnSelfSent(
|
||||||
ChatKind.Tell,
|
ChatKind.Tell,
|
||||||
"reply",
|
"reply",
|
||||||
"Recipient");
|
logTextType: 0x04u,
|
||||||
|
targetOrChannel: "Recipient");
|
||||||
communication.TurbineChat.OnChannelsReceived(
|
communication.TurbineChat.OnChannelsReceived(
|
||||||
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
||||||
communication.Friends.Apply(new FriendsUpdate(
|
communication.Friends.Apply(new FriendsUpdate(
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,8 @@ public sealed class NoWindowGameRuntimeHostTests
|
||||||
"Runtime",
|
"Runtime",
|
||||||
"fixture speech",
|
"fixture speech",
|
||||||
localGuid,
|
localGuid,
|
||||||
isRanged: false);
|
isRanged: false,
|
||||||
|
logTextType: 0x02u);
|
||||||
runtime.CharacterOwner.LocalPlayer.OnVitalUpdate(
|
runtime.CharacterOwner.LocalPlayer.OnVitalUpdate(
|
||||||
vitalId: 1u,
|
vitalId: 1u,
|
||||||
ranks: 100u,
|
ranks: 100u,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ public sealed class ChatVMTests
|
||||||
public void RecentLines_ReturnsAllEntries_WhenBelowLimit()
|
public void RecentLines_ReturnsAllEntries_WhenBelowLimit()
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
log.OnLocalSpeech(sender: "Caith", text: "hello", senderGuid: 0x5000_0001u, 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);
|
log.OnLocalSpeech(sender: "Regal", text: "world", senderGuid: 0x5000_0002u, isRanged: false, logTextType: 0x02u);
|
||||||
|
|
||||||
var vm = new ChatVM(log, displayLimit: 20);
|
var vm = new ChatVM(log, displayLimit: 20);
|
||||||
var lines = vm.RecentLines();
|
var lines = vm.RecentLines();
|
||||||
|
|
@ -34,7 +34,7 @@ public sealed class ChatVMTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
for (int i = 0; i < 30; i++)
|
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 vm = new ChatVM(log, displayLimit: 5);
|
||||||
var lines = vm.RecentLines();
|
var lines = vm.RecentLines();
|
||||||
|
|
@ -136,24 +136,27 @@ public sealed class ChatVMTests
|
||||||
|
|
||||||
Assert.Empty(vm.RecentLines());
|
Assert.Empty(vm.RecentLines());
|
||||||
|
|
||||||
log.OnLocalSpeech("Caith", "hello", 0x5000_0001u, false);
|
log.OnLocalSpeech("Caith", "hello", 0x5000_0001u, false, logTextType: 0x02u);
|
||||||
var after = vm.RecentLines();
|
var after = vm.RecentLines();
|
||||||
Assert.Single(after);
|
Assert.Single(after);
|
||||||
Assert.Equal("Caith says, \"hello\"", after[0]);
|
Assert.Equal("Caith says, \"hello\"", after[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShowSystemMessage_UsesClientLocalLogTextType()
|
public void ShowSystemMessage_UsesDefaultLogTextType()
|
||||||
{
|
{
|
||||||
// Campaign CH slice CH1: client-local command output (/help, /clear,
|
// Corrected 2026-08-09, Opus review of 172c6f9a: ShowSystemMessage
|
||||||
// /framerate, /loc, ...) never reaches the wire — retail's own
|
// is ClientCommandController's general-purpose informational sink
|
||||||
// client-local text is LogTextType 0x1A (bright red).
|
// (/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 log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
|
|
||||||
vm.ShowSystemMessage("Unknown command: foo");
|
vm.ShowSystemMessage("Unknown command: foo");
|
||||||
|
|
||||||
var entry = Assert.Single(log.Snapshot());
|
var entry = Assert.Single(log.Snapshot());
|
||||||
Assert.Equal(0x1Au, entry.LogTextType);
|
Assert.Equal(0x00u, entry.LogTextType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ public sealed class ChatPanelInputTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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 panel = new ChatPanel(vm);
|
||||||
var bus = new RecordingBus();
|
var bus = new RecordingBus();
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ public sealed class ChatPanelLayoutTests
|
||||||
Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetScrollHereY");
|
Assert.DoesNotContain(renderer.Calls, c => c.Method == "SetScrollHereY");
|
||||||
|
|
||||||
// Append a new entry, render again — auto-scroll should fire.
|
// 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();
|
renderer.Calls.Clear();
|
||||||
panel.Render(ctx, renderer);
|
panel.Render(ctx, renderer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public sealed class ChatVMCombatTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
log.OnCombatLine("Mosswart hit you for 8 fire damage to your chest.",
|
log.OnCombatLine("Mosswart hit you for 8 fire damage to your chest.",
|
||||||
CombatLineKind.Warning);
|
logTextType: 0x06u, kind: CombatLineKind.Warning);
|
||||||
|
|
||||||
var lines = vm.RecentLinesDetailed();
|
var lines = vm.RecentLinesDetailed();
|
||||||
var line = Assert.Single(lines);
|
var line = Assert.Single(lines);
|
||||||
|
|
@ -48,7 +48,7 @@ public sealed class ChatVMCombatTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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());
|
var line = Assert.Single(vm.RecentLinesDetailed());
|
||||||
Assert.Equal(ChatKind.LocalSpeech, line.Kind);
|
Assert.Equal(ChatKind.LocalSpeech, line.Kind);
|
||||||
|
|
@ -61,9 +61,9 @@ public sealed class ChatVMCombatTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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%).",
|
log.OnCombatLine("You hit Mosswart for 5 slashing damage (50.0%).",
|
||||||
CombatLineKind.Info);
|
logTextType: 0x06u, kind: CombatLineKind.Info);
|
||||||
|
|
||||||
var panel = new ChatPanel(vm);
|
var panel = new ChatPanel(vm);
|
||||||
var bus = new RecordingChatBus();
|
var bus = new RecordingChatBus();
|
||||||
|
|
@ -74,8 +74,9 @@ public sealed class ChatVMCombatTests
|
||||||
// Plain LocalSpeech entry → Text; combat entry → TextColored, now
|
// Plain LocalSpeech entry → Text; combat entry → TextColored, now
|
||||||
// sourced from RetailChatColorTable (Campaign CH slice CH1) keyed
|
// sourced from RetailChatColorTable (Campaign CH slice CH1) keyed
|
||||||
// by LogTextType, not ChatPanel.ColorForCombat's severity bucket.
|
// by LogTextType, not ChatPanel.ColorForCombat's severity bucket.
|
||||||
// OnCombatLine's default logTextType (0x06, generic Combat slot,
|
// The 0x06 generic Combat slot (colorDarkRed) is passed explicitly
|
||||||
// colorDarkRed) applies here since no explicit type was passed.
|
// above — a registered approximation of retail's per-message
|
||||||
|
// dispatch (register row AP-176), not a ChatLog default.
|
||||||
Assert.Contains(renderer.Calls, c =>
|
Assert.Contains(renderer.Calls, c =>
|
||||||
c.Method == "Text" && (string?)c.Args[0] == "Alice says, \"hi\"");
|
c.Method == "Text" && (string?)c.Args[0] == "Alice says, \"hi\"");
|
||||||
var coloredCall = Assert.Single(
|
var coloredCall = Assert.Single(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ public sealed class ChatVMLastTellSenderTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
log.OnTellReceived("Before", "ping", 0x5000_0001u);
|
log.OnTellReceived("Before", "ping", 0x5000_0001u, logTextType: 0x03u);
|
||||||
|
|
||||||
vm.Dispose();
|
vm.Dispose();
|
||||||
vm.Dispose();
|
vm.Dispose();
|
||||||
log.OnTellReceived("After", "pong", 0x5000_0002u);
|
log.OnTellReceived("After", "pong", 0x5000_0002u, logTextType: 0x03u);
|
||||||
|
|
||||||
Assert.Equal("Before", vm.LastIncomingTellSender);
|
Assert.Equal("Before", vm.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +41,7 @@ public sealed class ChatVMLastTellSenderTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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);
|
Assert.Equal("Bestie", vm.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
@ -52,8 +52,8 @@ public sealed class ChatVMLastTellSenderTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
|
|
||||||
log.OnTellReceived("Bestie", "ping", 0x5000_00AAu);
|
log.OnTellReceived("Bestie", "ping", 0x5000_00AAu, logTextType: 0x03u);
|
||||||
log.OnTellReceived("Regal", "yo", 0x5000_00BBu);
|
log.OnTellReceived("Regal", "yo", 0x5000_00BBu, logTextType: 0x03u);
|
||||||
|
|
||||||
Assert.Equal("Regal", vm.LastIncomingTellSender);
|
Assert.Equal("Regal", vm.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
@ -66,8 +66,8 @@ public sealed class ChatVMLastTellSenderTests
|
||||||
|
|
||||||
// /r reply echo: SenderGuid = 0 (no real GUID for ourselves).
|
// /r reply echo: SenderGuid = 0 (no real GUID for ourselves).
|
||||||
// Must NOT clobber the captured sender.
|
// Must NOT clobber the captured sender.
|
||||||
log.OnTellReceived("Bestie", "ping", 0x5000_00AAu);
|
log.OnTellReceived("Bestie", "ping", 0x5000_00AAu, logTextType: 0x03u);
|
||||||
log.OnSelfSent(ChatKind.Tell, "back at you", targetOrChannel: "Bestie");
|
log.OnSelfSent(ChatKind.Tell, "back at you", logTextType: 0x04u, targetOrChannel: "Bestie");
|
||||||
|
|
||||||
Assert.Equal("Bestie", vm.LastIncomingTellSender);
|
Assert.Equal("Bestie", vm.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ public sealed class ChatVMLastTellSenderTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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);
|
Assert.Null(vm.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public sealed class ChatVMRetellAndProvidersTests
|
||||||
using var second = new ChatVM(log, commandTargets: targets);
|
using var second = new ChatVM(log, commandTargets: targets);
|
||||||
|
|
||||||
first.Dispose();
|
first.Dispose();
|
||||||
log.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
log.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u);
|
||||||
|
|
||||||
Assert.Equal("Bestie", second.LastIncomingTellSender);
|
Assert.Equal("Bestie", second.LastIncomingTellSender);
|
||||||
}
|
}
|
||||||
|
|
@ -32,8 +32,8 @@ public sealed class ChatVMRetellAndProvidersTests
|
||||||
{
|
{
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
log.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
log.OnTellReceived("Bestie", "incoming", 0x50000001u, logTextType: 0x03u);
|
||||||
log.OnSelfSent(ChatKind.Tell, "outgoing", targetOrChannel: "Caith");
|
log.OnSelfSent(ChatKind.Tell, "outgoing", logTextType: 0x04u, targetOrChannel: "Caith");
|
||||||
Assert.NotNull(vm.LastIncomingTellSender);
|
Assert.NotNull(vm.LastIncomingTellSender);
|
||||||
Assert.NotNull(vm.LastOutgoingTellTarget);
|
Assert.NotNull(vm.LastOutgoingTellTarget);
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ public sealed class ChatVMRetellAndProvidersTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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);
|
Assert.Equal("Caith", vm.LastOutgoingTellTarget);
|
||||||
// Inbound-tell tracker must NOT pick up an outgoing echo —
|
// Inbound-tell tracker must NOT pick up an outgoing echo —
|
||||||
|
|
@ -72,7 +72,7 @@ public sealed class ChatVMRetellAndProvidersTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
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.Equal("Bestie", vm.LastIncomingTellSender);
|
||||||
Assert.Null(vm.LastOutgoingTellTarget);
|
Assert.Null(vm.LastOutgoingTellTarget);
|
||||||
|
|
@ -84,8 +84,8 @@ public sealed class ChatVMRetellAndProvidersTests
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
|
|
||||||
log.OnSelfSent(ChatKind.Tell, "hi", targetOrChannel: "Caith");
|
log.OnSelfSent(ChatKind.Tell, "hi", logTextType: 0x04u, targetOrChannel: "Caith");
|
||||||
log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042);
|
log.OnTellReceived("Bestie", "psst", senderGuid: 0x5000_0042, logTextType: 0x03u);
|
||||||
|
|
||||||
Assert.Equal("Caith", vm.LastOutgoingTellTarget);
|
Assert.Equal("Caith", vm.LastOutgoingTellTarget);
|
||||||
Assert.Equal("Bestie", vm.LastIncomingTellSender);
|
Assert.Equal("Bestie", vm.LastIncomingTellSender);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue