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>
162 lines
6 KiB
C#
162 lines
6 KiB
C#
using AcDream.Core.Chat;
|
|
using AcDream.UI.Abstractions.Panels.Chat;
|
|
|
|
namespace AcDream.UI.Abstractions.Tests;
|
|
|
|
public sealed class ChatVMTests
|
|
{
|
|
[Fact]
|
|
public void RecentLines_ReturnsEmpty_ForEmptyLog()
|
|
{
|
|
var log = new ChatLog();
|
|
var vm = new ChatVM(log);
|
|
|
|
Assert.Empty(vm.RecentLines());
|
|
}
|
|
|
|
[Fact]
|
|
public void RecentLines_ReturnsAllEntries_WhenBelowLimit()
|
|
{
|
|
var log = new ChatLog();
|
|
log.OnLocalSpeech(sender: "Caith", text: "hello", senderGuid: 0x5000_0001u, isRanged: false, logTextType: 0x02u);
|
|
log.OnLocalSpeech(sender: "Regal", text: "world", senderGuid: 0x5000_0002u, isRanged: false, logTextType: 0x02u);
|
|
|
|
var vm = new ChatVM(log, displayLimit: 20);
|
|
var lines = vm.RecentLines();
|
|
|
|
Assert.Equal(2, lines.Count);
|
|
Assert.Equal("Caith says, \"hello\"", lines[0]);
|
|
Assert.Equal("Regal says, \"world\"", lines[1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecentLines_ReturnsTail_WhenAboveLimit_InOldestFirstOrder()
|
|
{
|
|
var log = new ChatLog();
|
|
for (int i = 0; i < 30; i++)
|
|
log.OnLocalSpeech(sender: "A", text: $"msg{i}", senderGuid: 0x5000_0001u, isRanged: false, logTextType: 0x02u);
|
|
|
|
var vm = new ChatVM(log, displayLimit: 5);
|
|
var lines = vm.RecentLines();
|
|
|
|
// Tail = msg25..msg29 (5 entries, oldest first).
|
|
Assert.Equal(5, lines.Count);
|
|
Assert.Equal("A says, \"msg25\"", lines[0]);
|
|
Assert.Equal("A says, \"msg29\"", lines[4]);
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_LocalSpeech_RetailStyleSays()
|
|
{
|
|
// Retail format: "Name says, \"text\"" for someone else;
|
|
// "You say, \"text\"" for our own /say (sender == "" or "You").
|
|
var incoming = new ChatEntry(ChatKind.LocalSpeech, "Caith", "hello", 0x5000_0001u, 0);
|
|
Assert.Equal("Caith says, \"hello\"", ChatVM.FormatEntry(incoming));
|
|
|
|
var ownEcho = new ChatEntry(ChatKind.LocalSpeech, "", "hi there", 0, 0);
|
|
Assert.Equal("You say, \"hi there\"", ChatVM.FormatEntry(ownEcho));
|
|
|
|
var ownEchoSubst = new ChatEntry(ChatKind.LocalSpeech, "You", "shouted echo", 0, 0);
|
|
Assert.Equal("You say, \"shouted echo\"", ChatVM.FormatEntry(ownEchoSubst));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_RangedSpeech_RetailStyleShouts()
|
|
{
|
|
var incoming = new ChatEntry(ChatKind.RangedSpeech, "Caith", "hello", 0x5000_0001u, 0);
|
|
Assert.Equal("Caith shouts, \"hello\"", ChatVM.FormatEntry(incoming));
|
|
|
|
var ownEcho = new ChatEntry(ChatKind.RangedSpeech, "You", "loud", 0, 0);
|
|
Assert.Equal("You shout, \"loud\"", ChatVM.FormatEntry(ownEcho));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_Channel_UsesChannelNameWhenPresent()
|
|
{
|
|
// Friendly name takes precedence — "[Trade] Caith says, \"...\""
|
|
var named = new ChatEntry(ChatKind.Channel, "Caith", "g'day", 0x5000_0001u, 7u)
|
|
{
|
|
ChannelName = "Trade",
|
|
};
|
|
Assert.Equal("[Trade] Caith says, \"g'day\"", ChatVM.FormatEntry(named));
|
|
|
|
// Falls back to "ch {id}" when ChannelName isn't set (legacy
|
|
// path / older callers).
|
|
var unnamed = new ChatEntry(ChatKind.Channel, "Caith", "g'day", 0x5000_0001u, 7u);
|
|
Assert.Equal("[ch 7] Caith says, \"g'day\"", ChatVM.FormatEntry(unnamed));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_Tell_RetailStyleTells()
|
|
{
|
|
// SenderGuid != 0 -> incoming whisper -> "Regal tells you, ..."
|
|
var incoming = new ChatEntry(ChatKind.Tell, "Regal", "psst", 0x5000_0002u, 0);
|
|
Assert.Equal("Regal tells you, \"psst\"", ChatVM.FormatEntry(incoming));
|
|
|
|
// SenderGuid == 0 -> our own outbound echo -> Sender carries
|
|
// the target name -> "You tell Regal, ..."
|
|
var ownEcho = new ChatEntry(ChatKind.Tell, "Regal", "psst", 0, 0);
|
|
Assert.Equal("You tell Regal, \"psst\"", ChatVM.FormatEntry(ownEcho));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_System_NoSenderShown()
|
|
{
|
|
var entry = new ChatEntry(ChatKind.System, Sender: "", "Your spell fizzled!", 0, 0);
|
|
Assert.Equal("[System] Your spell fizzled!", ChatVM.FormatEntry(entry));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatEntry_Popup_Prefixed()
|
|
{
|
|
var entry = new ChatEntry(ChatKind.Popup, Sender: "", "A door stands before you.", 0, 0);
|
|
Assert.Equal("[Popup] A door stands before you.", ChatVM.FormatEntry(entry));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsOnNullLog()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => new ChatVM(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsOnZeroOrNegativeLimit()
|
|
{
|
|
var log = new ChatLog();
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new ChatVM(log, displayLimit: 0));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new ChatVM(log, displayLimit: -1));
|
|
}
|
|
|
|
[Fact]
|
|
public void RecentLines_ReturnsNewLineData_AfterSubsequentAppend()
|
|
{
|
|
// Confirm the VM isn't caching — each call re-snapshots the log.
|
|
var log = new ChatLog();
|
|
var vm = new ChatVM(log);
|
|
|
|
Assert.Empty(vm.RecentLines());
|
|
|
|
log.OnLocalSpeech("Caith", "hello", 0x5000_0001u, false, logTextType: 0x02u);
|
|
var after = vm.RecentLines();
|
|
Assert.Single(after);
|
|
Assert.Equal("Caith says, \"hello\"", after[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowSystemMessage_UsesDefaultLogTextType()
|
|
{
|
|
// Corrected 2026-08-09, Opus review of 172c6f9a: ShowSystemMessage
|
|
// is ClientCommandController's general-purpose informational sink
|
|
// (/help, /clear, /framerate, /loc, @version, friends list, ...).
|
|
// Retail types the great majority of that command output 0x00
|
|
// Default (green); 0x1A (bright red) is reserved for genuine
|
|
// refusals, which land separately with CH2's SpewBox routing.
|
|
var log = new ChatLog();
|
|
var vm = new ChatVM(log);
|
|
|
|
vm.ShowSystemMessage("Unknown command: foo");
|
|
|
|
var entry = Assert.Single(log.Snapshot());
|
|
Assert.Equal(0x00u, entry.LogTextType);
|
|
}
|
|
}
|