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>
161 lines
7.2 KiB
C#
161 lines
7.2 KiB
C#
using System.Collections.Generic;
|
|
|
|
namespace AcDream.Core.Chat;
|
|
|
|
/// <summary>
|
|
/// Translates ACE <c>WeenieError</c> + <c>WeenieErrorWithString</c> codes
|
|
/// into the human-readable templates the retail client showed. The
|
|
/// retail client baked these strings into <c>string_table.bin</c>; for
|
|
/// our purposes we mirror ACE's enum-doc comments
|
|
/// (<c>references/ACE/Source/ACE.Entity/Enum/WeenieError.cs</c> +
|
|
/// <c>WeenieErrorWithString.cs</c>) since they preserve the original
|
|
/// templates verbatim, including the literal <c>_</c> placeholder where
|
|
/// the parameter goes.
|
|
///
|
|
/// <para>
|
|
/// <b>Why we need this:</b> Many of the codes ACE sends — especially
|
|
/// the high-frequency ones the user actually sees in normal play —
|
|
/// are <i>informational</i>, not error-level (e.g. <c>0x051B</c> =
|
|
/// "You have entered the X channel.", <c>0x051D</c> = "Turbine Chat
|
|
/// is enabled."). Displaying them as <c>WeenieError 0xNNNN</c> is
|
|
/// noisy and misleading. With the proper template they read as the
|
|
/// retail player would have seen them.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// We don't translate every code — only the ~30 most likely to appear
|
|
/// in a normal session. Unknown codes fall back to the
|
|
/// <c>WeenieError 0xNNNN[: param]</c> form so nothing is silently
|
|
/// lost. New codes can be added in 30 seconds when the user reports
|
|
/// one.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class WeenieErrorMessages
|
|
{
|
|
/// <summary>
|
|
/// Format a WeenieError / WeenieErrorWithString into a human-readable
|
|
/// system-message string.
|
|
/// </summary>
|
|
/// <param name="errorCode">The wire error code.</param>
|
|
/// <param name="param">The interpolated substring (null for plain
|
|
/// <c>WeenieError</c>, set for <c>WeenieErrorWithString</c>).</param>
|
|
public static string Format(uint errorCode, string? param)
|
|
{
|
|
// WeenieErrorWithString templates use the literal underscore
|
|
// character `_` as the placeholder for the param. We
|
|
// substitute it for `param` if present, otherwise drop it.
|
|
if (param is not null && WithStringTemplates.TryGetValue(errorCode, out var withTemplate))
|
|
{
|
|
return withTemplate.Replace("_", param);
|
|
}
|
|
|
|
if (NoParamTemplates.TryGetValue(errorCode, out var template))
|
|
{
|
|
// Some "no-param" codes do still arrive with a meaningless
|
|
// param string; ignore it.
|
|
return template;
|
|
}
|
|
|
|
// Unknown code — fall back to the raw form.
|
|
return string.IsNullOrEmpty(param)
|
|
? $"WeenieError 0x{errorCode:X4}"
|
|
: $"WeenieError 0x{errorCode:X4}: {param}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Codes from <c>WeenieError</c> (no param). Templates copied
|
|
/// verbatim from ACE enum-doc comments.
|
|
/// </summary>
|
|
private static readonly Dictionary<uint, string> NoParamTemplates = new()
|
|
{
|
|
// Command parser
|
|
[0x0026] = "That is not a valid command.", // ThatIsNotAValidCommand
|
|
|
|
// Tell-related
|
|
[0x052B] = "That person is not available now.", // CharacterNotAvailable
|
|
|
|
// Chat-channel related
|
|
[0x051D] = "Turbine Chat is enabled.", // TurbineChatIsEnabled
|
|
|
|
// Trade
|
|
[0x0529] = "Trade Complete!", // TradeComplete
|
|
|
|
// Allegiance / Fellowship membership errors (high-frequency
|
|
// when player isn't in either group and tries the channel)
|
|
[0x0414] = "You are not in an allegiance!", // YouAreNotInAllegiance
|
|
[0x050F] = "You do not belong to a Fellowship.", // YouDoNotBelongToAFellowship
|
|
|
|
// Allegiance
|
|
[0x0496] = "Your Allegiance has been dissolved!", // YourAllegianceHasBeenDissolved
|
|
[0x0497] = "Your patron's Allegiance to you has been broken!",
|
|
// YourPatronsAllegianceHasBeenBroken
|
|
[0x0535] = "You do not have the authority within your allegiance to do that.",
|
|
|
|
// Movement / teleport / housing
|
|
[0x0498] = "You have moved too far!", // YouHaveMovedTooFar
|
|
[0x0499] = "That is not a valid destination!", // TeleToInvalidPosition
|
|
[0x0532] = "You must wait 30 days after purchasing a house before you may purchase another with any character on the same account.",
|
|
|
|
// Fellowship
|
|
[0x0528] = "The fellowship is locked; you cannot open locked fellowships.",
|
|
|
|
// Player flavour
|
|
[0x0526] = "You chicken out.", // YouChickenOut
|
|
};
|
|
|
|
/// <summary>
|
|
/// Codes from <c>WeenieErrorWithString</c>. Templates copied
|
|
/// verbatim from ACE enum-doc comments. The <c>_</c> placeholder
|
|
/// is substituted with the param at format time.
|
|
/// </summary>
|
|
private static readonly Dictionary<uint, string> WithStringTemplates = new()
|
|
{
|
|
// Channel join / leave (high frequency at login)
|
|
[0x051B] = "You have entered the _ channel.", // YouHaveEnteredThe_Channel
|
|
[0x051C] = "You have left the _ channel.", // YouHaveLeftThe_Channel
|
|
|
|
// Chat-server failures
|
|
[0x051E] = "_ will not receive your message, please use urgent assistance to speak with an in-game representative.",
|
|
[0x051F] = "Message Blocked: _", // MessageBlocked_
|
|
|
|
// Hear / loud-list
|
|
[0x0521] = "_ has been added to the list of people you can hear.",
|
|
[0x0522] = "_ has been removed from the list of people you can hear.",
|
|
[0x0525] = "You fail to remove _ from your loud list.",
|
|
|
|
// Snooping (admin)
|
|
[0x052C] = "You are now snooping on _.",
|
|
[0x052D] = "You are no longer snooping on _.",
|
|
[0x052E] = "You fail to snoop on _.",
|
|
[0x052F] = "_ attempted to snoop on you.",
|
|
[0x0551] = "You are not listening to the _ channel.",
|
|
|
|
// Allegiance
|
|
[0x046A] = "_ doesn't know what to do with that.",
|
|
[0x0413] = "_ is already one of your followers.",
|
|
[0x0416] = "_ cannot have any more Vassals.",
|
|
[0x03EF] = "_ is not accepting gifts right now.",
|
|
|
|
// Combat / spell failures
|
|
[0x004E] = "You fail to affect _ because you cannot affect anyone!",
|
|
[0x004F] = "You fail to affect _ because they cannot be harmed!",
|
|
[0x0050] = "You fail to affect _ because beneficial spells do not affect them!",
|
|
[0x0051] = "You fail to affect _ because you are not a player killer!",
|
|
[0x0052] = "You fail to affect _ because they are not a player killer!",
|
|
[0x0053] = "You fail to affect _ because you are not the same sort of player killer as them!",
|
|
[0x0054] = "You fail to affect _ because you are acting across a house boundary!",
|
|
|
|
// Inventory / etiquette
|
|
[0x001E] = "_ is too busy to accept gifts right now.",
|
|
[0x002B] = "_ cannot carry anymore.",
|
|
|
|
// Fellowship
|
|
[0x0517] = "_ is not close enough to your level.",
|
|
[0x0518] = "This fellowship is locked; _ cannot be recruited into the fellowship.",
|
|
|
|
// Hooks
|
|
[0x0510] = "Maximum number of _ hooked.",
|
|
[0x0514] = "Maximum number of _ hooked until one is removed.",
|
|
[0x0515] = "You no longer have the maximum number of _ hooked. You may hook additional.",
|
|
};
|
|
}
|