Retail colors chat lines by the 34-value wire LogTextType (ACE's ChatMessageType), NOT by acdream's synthetic 9-value ChatKind. The old ChatWindowController.RetailChatColor(ChatKind) collapsed distinct retail colors onto one bucket per ChatKind — e.g. every Channel line rendered colorLightBlue (Magic's slot) when retail's actual palette spans five different colors across the Turbine rooms and legacy allegiance family. Ports ChatInterface::BuildChatColorLookupTable @0x004F31C0 verbatim (RetailChatColorTable, all 34 RGBA floats read from the PDB-paired binary's .data section) and threads a new ChatEntry.LogTextType field through every ingestion site to the correct retail wire value: HearSpeech/Tell pass the wire chatType through verbatim; Emote/SoulEmote hard-code 0x0C; the Tell self-echo hard-codes 0x04; legacy ChatChannel broadcasts derive their type from the channel bit via the new LegacyChannelChatType helper (ported from the decompiled Handle_Communication__ChannelBroadcast dispatch, hear vs. own-send); TurbineChat rooms map through TurbineChatDisplayNames.LogTextType; CombatChatTranslator's hit/miss/evade lines map to ACE's CombatSelf/ CombatEnemy per Player_Combat.cs; kill/death lines use retail's decompiled 0x00 Default (not a combat color). ChatWindowController's transcript now folds LogTextType through RetailChatColorTable with retail's exact "out-of-range keeps the previous line's color" carry rule; ChatPanel's combat highlighting sources the same table. Corrects HearSpeech.cs's doc-comment ChatType legend (4 of 6 entries were wrong). Adds register row AP-175 for the pre-existing (unchanged) Popup-renders-in-chat divergence and updates AP-39's stale per-ChatKind description. Narrows ISSUES #139 — its chat-colors half is done. Retail renders no chat timestamp prefix path exists in acdream today, so the "timestamp is always colorGrey 0x0C" rule has nothing to attach to; noted here per the research doc rather than left silent. Research: docs/research/2026-08-09-chat-retail-color-table.md Full Release suite: 11,833 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
5.7 KiB
C#
159 lines
5.7 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);
|
|
log.OnLocalSpeech(sender: "Regal", text: "world", senderGuid: 0x5000_0002u, isRanged: false);
|
|
|
|
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);
|
|
|
|
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);
|
|
var after = vm.RecentLines();
|
|
Assert.Single(after);
|
|
Assert.Equal("Caith says, \"hello\"", after[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowSystemMessage_UsesClientLocalLogTextType()
|
|
{
|
|
// Campaign CH slice CH1: client-local command output (/help, /clear,
|
|
// /framerate, /loc, ...) never reaches the wire — retail's own
|
|
// client-local text is LogTextType 0x1A (bright red).
|
|
var log = new ChatLog();
|
|
var vm = new ChatVM(log);
|
|
|
|
vm.ShowSystemMessage("Unknown command: foo");
|
|
|
|
var entry = Assert.Single(log.Snapshot());
|
|
Assert.Equal(0x1Au, entry.LogTextType);
|
|
}
|
|
}
|