fix(chat): /help client-side handler + System dedup + ThatIsNotAValidCommand template
Phase J follow-up after a 2026-04-25 trace where typing /help
produced two identical "Unknown command: help" lines (ACE fires the
text via both GameMessageSystemChat 0xF7E0 and a paired
CommunicationTransientString 0x02EB), and the server's WeenieError
0x0026 trailer rendered cryptically as "WeenieError 0x0026".
Three small changes:
1. WeenieErrorMessages: add 0x0026 ThatIsNotAValidCommand ->
"That is not a valid command." Plus 0x0414 / 0x050F that Phase J
already added are now covered by tests too.
2. ChatLog.OnSystemMessage dedup. Track last system text + arrival
time; if a second identical text shows up within 1 second,
suppress. ACE's two-path send (gag warnings, command errors,
etc.) collapses to a single chat line. Long bursts of repeated
text still skip the duplicates without resetting the timer.
3. Client-side /help and /clear in ChatPanel. Intercepted BEFORE
the parser passes to the server bus:
- /help, /?, /h (case-insensitive) -> render local cheat-sheet
listing acdream's slash prefixes via ChatLog.OnSystemMessage.
Avoids the round-trip to ACE that produced the duplicate
"Unknown command: help" lines AND gives users discoverability.
- /clear, /cls -> drains the chat log so the panel starts empty.
New ChatVM.ShowSystemMessage() + ChatVM.Clear() expose the
minimum surface the panel needs to dispatch client-only feedback
without coupling the panel to ChatLog directly.
12 new tests:
- 3 WeenieErrorMessages template adds (0x0026 / 0x0414 / 0x050F).
- 4 ChatLog dedup cases (immediate dup, different text, triplet,
bookended-by-different-text).
- 5 ChatPanel client-command cases (/help, 3 alias variants,
/clear).
Solution total: 1033 green (243 Core.Net + 130 UI + 660 Core),
0 warnings.
Acceptance: type /help in chat -> local help banner appears, no
server round-trip, no "Unknown command: help" duplicates. Type
/clear -> chat tail empty. Welcome banner + WeenieError-templated
"You are not in an allegiance!" / "You do not belong to a
Fellowship." continue rendering once each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7726f62528
commit
3501194083
7 changed files with 269 additions and 2 deletions
61
tests/AcDream.Core.Tests/Chat/ChatLogSystemDedupTests.cs
Normal file
61
tests/AcDream.Core.Tests/Chat/ChatLogSystemDedupTests.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using AcDream.Core.Chat;
|
||||
|
||||
namespace AcDream.Core.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Phase J follow-up: ACE often sends the same system text via two
|
||||
/// wire paths (GameMessageSystemChat 0xF7E0 + GameEventCommunication-
|
||||
/// TransientString 0x02EB) for back-compat — both get routed to
|
||||
/// <see cref="ChatLog.OnSystemMessage"/> in our wiring, which would
|
||||
/// double-print every system line. Dedupe consecutive identical
|
||||
/// system text within a short window.
|
||||
/// </summary>
|
||||
public sealed class ChatLogSystemDedupTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnSystemMessage_ImmediateDuplicate_OnlyOneEntry()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
|
||||
// Second identical message arrived <1s later — suppressed.
|
||||
Assert.Single(log.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnSystemMessage_DifferentText_BothEntries()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
log.OnSystemMessage("Welcome to Asheron's Call", chatType: 0);
|
||||
log.OnSystemMessage("Use @acecommands to get a complete list of commands.", chatType: 0);
|
||||
|
||||
// Different text — both retained even back-to-back.
|
||||
Assert.Equal(2, log.Snapshot().Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnSystemMessage_TripletDuplicate_StillOnlyOneEntry()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
|
||||
Assert.Single(log.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnSystemMessage_DuplicateBookendingNonDuplicate_KeepsBothUnique()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
log.OnSystemMessage("Welcome to Asheron's Call", chatType: 0);
|
||||
log.OnSystemMessage("Unknown command: help", chatType: 0);
|
||||
|
||||
// First "Unknown..." retained, "Welcome..." retained, second
|
||||
// "Unknown..." is no longer the *immediate* duplicate (the
|
||||
// most-recent entry is "Welcome..."), so it's kept too.
|
||||
Assert.Equal(3, log.Snapshot().Length);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,33 @@ public sealed class WeenieErrorMessagesTests
|
|||
Assert.Equal("Trade Complete!", WeenieErrorMessages.Format(0x0529, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Format_ThatIsNotAValidCommand()
|
||||
{
|
||||
// 0x0026 fires on /-prefixed text that ACE's command parser
|
||||
// can't resolve. Filed after a 2026-04-25 trace where /help
|
||||
// produced cryptic "WeenieError 0x0026" lines.
|
||||
Assert.Equal(
|
||||
"That is not a valid command.",
|
||||
WeenieErrorMessages.Format(0x0026, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Format_YouAreNotInAllegiance()
|
||||
{
|
||||
Assert.Equal(
|
||||
"You are not in an allegiance!",
|
||||
WeenieErrorMessages.Format(0x0414, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Format_YouDoNotBelongToAFellowship()
|
||||
{
|
||||
Assert.Equal(
|
||||
"You do not belong to a Fellowship.",
|
||||
WeenieErrorMessages.Format(0x050F, null));
|
||||
}
|
||||
|
||||
// ── unknown codes — graceful fallback preserves debug info ───────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,75 @@ public sealed class ChatPanelInputTests
|
|||
public void Publish<T>(T command) where T : notnull => Published.Add(command);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Submit_HelpCommand_RendersLocalHelpAndDoesNotPublish()
|
||||
{
|
||||
// Phase J follow-up: client-side commands (/help, /?, /h) are
|
||||
// intercepted before the parser. They render a local cheat-sheet
|
||||
// via ChatLog.OnSystemMessage and do NOT round-trip the server
|
||||
// — that's what prevented the "Unknown command: help" duplicate
|
||||
// ACE was firing back.
|
||||
var log = new ChatLog();
|
||||
var vm = new ChatVM(log);
|
||||
var panel = new ChatPanel(vm);
|
||||
var bus = new RecordingBus();
|
||||
var renderer = new FakePanelRenderer
|
||||
{
|
||||
InputTextSubmitNextSubmitted = "/help",
|
||||
InputTextSubmitNextBufferAfter = "",
|
||||
};
|
||||
|
||||
panel.Render(new PanelContext(0.016f, bus), renderer);
|
||||
|
||||
Assert.Empty(bus.Published);
|
||||
var entry = Assert.Single(log.Snapshot());
|
||||
Assert.Equal(ChatKind.System, entry.Kind);
|
||||
Assert.Contains("acdream chat commands:", entry.Text);
|
||||
Assert.Contains("/tell", entry.Text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/?")]
|
||||
[InlineData("/h")]
|
||||
[InlineData("/HELP")]
|
||||
public void Submit_HelpAliases_AlsoRenderLocalHelp(string raw)
|
||||
{
|
||||
var log = new ChatLog();
|
||||
var vm = new ChatVM(log);
|
||||
var panel = new ChatPanel(vm);
|
||||
var bus = new RecordingBus();
|
||||
var renderer = new FakePanelRenderer
|
||||
{
|
||||
InputTextSubmitNextSubmitted = raw,
|
||||
InputTextSubmitNextBufferAfter = "",
|
||||
};
|
||||
|
||||
panel.Render(new PanelContext(0.016f, bus), renderer);
|
||||
|
||||
Assert.Empty(bus.Published);
|
||||
Assert.Single(log.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Submit_ClearCommand_DrainsLog_AndDoesNotPublish()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
log.OnSystemMessage("seed line", chatType: 0);
|
||||
var vm = new ChatVM(log);
|
||||
var panel = new ChatPanel(vm);
|
||||
var bus = new RecordingBus();
|
||||
var renderer = new FakePanelRenderer
|
||||
{
|
||||
InputTextSubmitNextSubmitted = "/clear",
|
||||
InputTextSubmitNextBufferAfter = "",
|
||||
};
|
||||
|
||||
panel.Render(new PanelContext(0.016f, bus), renderer);
|
||||
|
||||
Assert.Empty(bus.Published);
|
||||
Assert.Empty(log.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Submit_PlainText_PublishesSayCommand()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue