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
|
|
@ -345,7 +345,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.Actions.Selection.SelectedObjectId ?? 0u,
|
||||
stackSplitQuantity: d.StackSplitQuantity,
|
||||
systemMessage:
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal),
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
session.CurrentSession?.SendPutItemInContainer(
|
||||
item,
|
||||
|
|
@ -415,7 +415,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
target,
|
||||
spellId),
|
||||
displayMessage:
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal),
|
||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||
canSend: () => late.Session.IsInWorld);
|
||||
|
||||
|
|
@ -753,7 +753,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
iconComposer.GetIcon,
|
||||
itemInteraction,
|
||||
d.Actions.Selection,
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
||||
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)),
|
||||
Cursor: new RetailUiCursorBindings(cursorFeedback, cursorManager),
|
||||
Confirmations: new ConfirmationRuntimeBindings(
|
||||
(type, context, accepted) =>
|
||||
|
|
@ -768,7 +768,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
item,
|
||||
inscription),
|
||||
text =>
|
||||
d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
||||
d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal)),
|
||||
StackSplitQuantity: d.StackSplitQuantity,
|
||||
Plugins: d.UiRegistry,
|
||||
Persistence: persistence,
|
||||
|
|
|
|||
|
|
@ -1125,7 +1125,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.Actions.CombatMode,
|
||||
d.Log,
|
||||
debugToast,
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au));
|
||||
text => d.Communication.Chat.OnSystemMessage(text, (uint)RetailLogTextType.ClientLocal));
|
||||
bindings.Adopt(
|
||||
"live combat-mode commands",
|
||||
d.CombatModeCommands.BindOwned(combatCommand));
|
||||
|
|
|
|||
|
|
@ -240,11 +240,11 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
|
|||
bindings.Chat.OnSelfSent(
|
||||
ChatKind.Tell,
|
||||
command.Text,
|
||||
targetOrChannel: command.TargetName,
|
||||
// Retail's own "You tell ..." echo is Speech_Direct_Send
|
||||
// (0x04), distinct from an incoming Tell's 0x03 — see
|
||||
// ChatMessageType.OutgoingTell's "You tell ..." comment.
|
||||
logTextType: 0x04u);
|
||||
logTextType: (uint)RetailLogTextType.SpeechDirectSend,
|
||||
targetOrChannel: command.TargetName);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +290,9 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
|
|||
targetOrChannel: legacy.Value.DisplayName,
|
||||
// Precise per-bit own-send type (LegacyChannelChatType.Resolve's
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -327,10 +327,15 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
ToggleUiLock: () =>
|
||||
_interaction.Settings.SetUiLocked(
|
||||
!_interaction.Settings.Gameplay.LockUI),
|
||||
// Client-local text (never reaches the wire), 0x1A — same
|
||||
// category as ChatVM.ShowSystemMessage.
|
||||
// ClientCommandController's informational output sink — 0x00
|
||||
// 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:
|
||||
text => _domain.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||
text => _domain.Communication.Chat.OnSystemMessage(text, 0x00u),
|
||||
ShowWeenieError:
|
||||
code => _domain.Communication.Chat.OnWeenieError(code, null),
|
||||
PlayerPublicWeenieBitfield: () =>
|
||||
|
|
|
|||
|
|
@ -461,7 +461,14 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
// appended line — an out-of-range LogTextType leaves it unchanged
|
||||
// rather than reverting to a default (research doc §3.2). Seed the
|
||||
// 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);
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
foreach (var d in detailed)
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ public static class GameEventWiring
|
|||
// ClientCombatSystem::HandleKillerNotificationEvent @0x0056C410
|
||||
// (pc:359548-359559), which calls AddTextToScroll(..., 0, 1, 0)
|
||||
// — 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 =>
|
||||
{
|
||||
|
|
@ -302,7 +302,7 @@ public static class GameEventWiring
|
|||
{
|
||||
var p = GameEvents.ParseKillerNotification(e.Payload.Span);
|
||||
// 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 ────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -98,10 +98,11 @@ public sealed class ChatLog
|
|||
/// (<c>speech.ChatType</c>) — passed through VERBATIM, with zero
|
||||
/// remapping, matching retail's <c>Handle_Communication__HearSpeech
|
||||
/// @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.
|
||||
/// </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
|
||||
// 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 —
|
||||
// ClientCommunicationSystem::HearEmote @0x0057CBE0, the
|
||||
// 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
|
||||
// hard-coded 0x0C.
|
||||
LogTextType = 0x0Cu,
|
||||
LogTextType = (uint)RetailLogTextType.Emote,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -171,14 +172,13 @@ public sealed class ChatLog
|
|||
SenderGuid: victimGuid,
|
||||
ChannelId: killerGuid)
|
||||
{
|
||||
// Inferred by analogy from the sibling GameEvents this opcode
|
||||
// shares a dispatch pattern with: VictimNotification (0x01AC)
|
||||
// and KillerNotification (0x01AD) both route through the SAME
|
||||
// retail handler, ClientCombatSystem::HandleKillerNotification
|
||||
// Event @0x0056C410 (cases 0xa/0xb of the combat-envelope
|
||||
// switch, pc:359548-359559), which calls
|
||||
// AddTextToScroll(..., 0, 1, 0) — type 0x00 Default. No direct
|
||||
// decomp citation was traced for 0x019E PlayerKilled itself.
|
||||
// Direct anchor (corrected 2026-08-09, Opus review of
|
||||
// 172c6f9a — replaces the earlier sibling-opcode analogy):
|
||||
// opcode 0x019E dispatches via UIQueueManager::ProcessNet
|
||||
// BlobData's byte table @0x55CB07 -> case 7 ->
|
||||
// ClientCombatSystem::HandlePlayerDeathEvent @0x0056C320 ->
|
||||
// AddTextToScroll(..., 0, 1, 0) @0x0056C3D8. Type 0x00 Default
|
||||
// confirmed.
|
||||
LogTextType = 0x00u,
|
||||
});
|
||||
}
|
||||
|
|
@ -216,8 +216,9 @@ public sealed class ChatLog
|
|||
// Retail's HandleFailureEvent @0x00571990 dispatches per ERROR
|
||||
// CODE across an ~87-case switch, mostly AddTextToScroll(...,
|
||||
// 0, ...) with a scattered handful at 0x1a (client-local red).
|
||||
// A full per-code port is future work; 0x00 (Default) matches
|
||||
// the switch's majority behavior and is the safe baseline.
|
||||
// A full per-code port is future work (register row AP-176);
|
||||
// 0x00 (Default) matches the switch's majority behavior and is
|
||||
// the safe baseline.
|
||||
LogTextType = 0x00u,
|
||||
});
|
||||
}
|
||||
|
|
@ -262,11 +263,11 @@ public sealed class ChatLog
|
|||
/// <summary>GameEvent Tell (0x02BD) — whisper received.</summary>
|
||||
/// <param name="logTextType">
|
||||
/// The wire <c>chatType</c> from the Tell GameEvent payload
|
||||
/// (<c>GameEvents.Tell.ChatType</c>) — retail's normal value is
|
||||
/// <c>0x03</c> (Tell), which is also this parameter's default for
|
||||
/// callers without a wire value in hand.
|
||||
/// (<c>GameEvents.Tell.ChatType</c>) — required; no production caller
|
||||
/// relies on a default. Retail's normal value is <c>0x03</c> (Tell)
|
||||
/// for callers without a wire value in hand.
|
||||
/// </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(
|
||||
Kind: ChatKind.Tell,
|
||||
|
|
@ -352,19 +353,26 @@ public sealed class ChatLog
|
|||
/// <c>warning().combat()</c> / <c>error().combat()</c> tag flow at
|
||||
/// <c>chat.rs:221-308</c>.
|
||||
/// </summary>
|
||||
/// <param name="text">The pre-formatted combat line text.</param>
|
||||
/// <param name="logTextType">
|
||||
/// The retail <c>LogTextType</c> for this combat line. Callers should
|
||||
/// pass one of the ACE-cited combat types (<c>0x16</c> Combat_Self for
|
||||
/// lines about the local player's OWN offensive action, <c>0x15</c>
|
||||
/// Combat_Enemy for lines about an enemy's action against the local
|
||||
/// player — see <see cref="CombatChatTranslator"/>) or <c>0x00</c>
|
||||
/// Default for retail's decompiled kill/death-notification color
|
||||
/// (<c>HandleKillerNotificationEvent @0x0056C410</c>). Defaults to
|
||||
/// <c>0x06</c> (the generic Combat slot) for callers with no more
|
||||
/// specific classification in hand.
|
||||
/// The retail <c>LogTextType</c> for this combat line — required; no
|
||||
/// production caller relies on a default. Callers should pass one of
|
||||
/// the ACE-cited combat types (<c>0x16</c> Combat_Self for lines about
|
||||
/// the local player's OWN offensive action, <c>0x15</c> Combat_Enemy
|
||||
/// for lines about an enemy's action against the local player — see
|
||||
/// <see cref="CombatChatTranslator"/>), <c>0x00</c> Default for
|
||||
/// retail's decompiled kill/death-notification color
|
||||
/// (<c>HandleKillerNotificationEvent @0x0056C410</c>), or <c>0x06</c>
|
||||
/// (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>
|
||||
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(
|
||||
Kind: ChatKind.Combat,
|
||||
|
|
@ -400,17 +408,16 @@ public sealed class ChatLog
|
|||
/// (a real incoming Tell carries the sender's player guid).
|
||||
/// </remarks>
|
||||
/// <param name="logTextType">
|
||||
/// The retail <c>LogTextType</c> for this self-sent line. When
|
||||
/// <see langword="null"/>, defaults to <c>0x04</c> Speech_Direct_Send
|
||||
/// for Tell (retail's own-echo "You tell ..." type — cross-check
|
||||
/// ACE's <c>ChatMessageType.OutgoingTell</c> comment "You tell ...") or
|
||||
/// <c>0x0B</c> Social_Send for Channel (the simplified own-send default
|
||||
/// research doc §3.3 records; the LiveSessionCommandRouter production
|
||||
/// caller overrides this with the precise per-channel-bit value from
|
||||
/// <see cref="LegacyChannelChatType.Resolve"/> instead of relying on
|
||||
/// this fallback).
|
||||
/// The retail <c>LogTextType</c> for this self-sent line — required;
|
||||
/// no production caller relies on a default (the old null-fallback
|
||||
/// ternary is retired). Pass <c>0x04</c> Speech_Direct_Send for Tell
|
||||
/// (retail's own-echo "You tell ..." type — cross-check ACE's
|
||||
/// <c>ChatMessageType.OutgoingTell</c> comment "You tell ...") or, for
|
||||
/// Channel, the precise per-channel-bit value from
|
||||
/// <see cref="LegacyChannelChatType.Resolve"/> — the
|
||||
/// LiveSessionCommandRouter production caller does exactly this.
|
||||
/// </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(
|
||||
Kind: kind,
|
||||
|
|
@ -425,7 +432,7 @@ public sealed class ChatLog
|
|||
ChannelId: 0)
|
||||
{
|
||||
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.
|
||||
"",
|
||||
FormatAttackConditionsSuffix(0));
|
||||
// Combat_Self (0x16): retail squelch-checks the attacker's OWN
|
||||
// outgoing-hit notification against ChatMessageType.CombatSelf —
|
||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:162-163
|
||||
// (GameEventAttackerNotification, "You hit X...").
|
||||
_chat.OnCombatLine(line, CombatLineKind.Info, logTextType: 0x16u);
|
||||
// Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred —
|
||||
// ClientCombatSystem::HandleAttackerNotificationEvent @0x0056B420
|
||||
// sets type 0x16 at @0x0056B761, the binary's only 0x16 producer
|
||||
// for this notification family. ACE's ChatMessageType.CombatSelf
|
||||
// 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)
|
||||
|
|
@ -142,33 +144,40 @@ public sealed class CombatChatTranslator : IDisposable
|
|||
sb.Append('.');
|
||||
if (e.Critical) sb.Append(" Critical hit.");
|
||||
sb.Append(FormatAttackConditionsSuffix(0));
|
||||
// Combat_Enemy (0x15): retail squelch-checks the defender's
|
||||
// incoming-hit notification against ChatMessageType.CombatEnemy —
|
||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:541
|
||||
// (GameEventDefenderNotification, "X hit you...").
|
||||
_chat.OnCombatLine(sb.ToString(), CombatLineKind.Warning, logTextType: 0x15u);
|
||||
// Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred —
|
||||
// ClientCombatSystem::HandleDefenderNotificationEvent @0x0056C920
|
||||
// sets type 0x15 at @0x0056D4B4, the binary's only 0x15 producer
|
||||
// for this notification family. ACE's ChatMessageType.CombatEnemy
|
||||
// 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)
|
||||
{
|
||||
// chat.rs:286-291 — EvasionAttackerNotification:
|
||||
// "{} evaded your attack."
|
||||
// Combat_Self (0x16): this is about the LOCAL PLAYER'S OWN attack
|
||||
// missing — retail squelch-checks GameEventEvasionAttackerNotification
|
||||
// against CombatSelf, same family as the hit-dealt line above —
|
||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:150.
|
||||
_chat.OnCombatLine($"{defenderName} evaded your attack.", CombatLineKind.Info, logTextType: 0x16u);
|
||||
// Combat_Self (0x16): decomp-CONFIRMED, not ACE-inferred —
|
||||
// ClientCombatSystem::HandleEvasionAttackerNotificationEvent
|
||||
// @0x0056C7A0 sets type 0x16 at @0x0056C870, the binary's only
|
||||
// 0x16 producer for this notification family (same slot as the
|
||||
// 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)
|
||||
{
|
||||
// chat.rs:292-297 — EvasionDefenderNotification:
|
||||
// "You evaded {}'s attack."
|
||||
// Combat_Enemy (0x15): this is about an ENEMY'S attack (that the
|
||||
// local player evaded) — retail squelch-checks
|
||||
// GameEventEvasionDefenderNotification against CombatEnemy —
|
||||
// references/ACE/Source/ACE.Server/WorldObjects/Player_Combat.cs:345.
|
||||
_chat.OnCombatLine($"You evaded {attackerName}'s attack.", CombatLineKind.Info, logTextType: 0x15u);
|
||||
// Combat_Enemy (0x15): decomp-CONFIRMED, not ACE-inferred —
|
||||
// ClientCombatSystem::HandleEvasionDefenderNotificationEvent
|
||||
// @0x0056C620 sets type 0x15 at @0x0056C710, the binary's only
|
||||
// 0x15 producer for this notification family. ACE's
|
||||
// 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)
|
||||
|
|
@ -185,7 +194,7 @@ public sealed class CombatChatTranslator : IDisposable
|
|||
// both via ClientCombatSystem::HandleKillerNotificationEvent
|
||||
// @0x0056C410) calls AddTextToScroll(..., 0, 1, 0) — see
|
||||
// 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) ───────────────────
|
||||
|
|
|
|||
|
|
@ -16,62 +16,94 @@ namespace AcDream.Core.Chat;
|
|||
/// </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
|
||||
/// the same for own-send — only Patron/Vassal/Follower get a distinct
|
||||
/// send-color; Fellowship, Co-Vassals, Allegiance Broadcast, and the
|
||||
/// generic/admin catch-all use the SAME type for hear and send):
|
||||
/// the same for own-send — only Patron/Vassal/Follower and the
|
||||
/// FellowBroadcast bit get a distinct send-color; Fellowship, Co-Vassals,
|
||||
/// and Allegiance Broadcast use the SAME type for hear and send):
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class LegacyChannelChatType
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve <paramref name="channelBit"/> (the <c>ChannelBroadcast</c>
|
||||
/// wire channel id, a single flag bit) to a <c>LogTextType</c>.
|
||||
/// Resolve <paramref name="channelId"/> (the <c>ChannelBroadcast</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>
|
||||
/// <param name="channelBit">The wire channel id / bitflag.</param>
|
||||
/// <param name="channelId">The wire channel id / bitflag.</param>
|
||||
/// <param name="ownSend">
|
||||
/// <see langword="true"/> when this is the local player's own outgoing
|
||||
/// message (retail's <c>m_buffer->m_len == 1</c> self-sentinel
|
||||
/// branch); <see langword="false"/> when hearing another sender.
|
||||
/// </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.
|
||||
// pc:00570e48 (hear, m_buffer_5=0x13) / pc:00570d08 (send, 0x13).
|
||||
0x0800u => 0x13u,
|
||||
0x0800u => (uint)RetailLogTextType.Fellowship,
|
||||
|
||||
// Patron / Vassal / Follower(Monarch): hear = Social (0xA,
|
||||
// "Your patron/vassal/follower X tells you..."); OWN send =
|
||||
// Social_Send (0xB, "You say to your patron/vassal/follower...").
|
||||
// "Your patron/vassal/follower X tells you..."); OWN send = Social_
|
||||
// Send (0xB, "You say to your patron/vassal/follower...").
|
||||
// pc:00570e50/00570e58 (Patron/Vassal hear, 0xa) + pc:00570e40
|
||||
// (Follower hear, 0xa); pc:00570c07/00570c21 (all three own-send
|
||||
// via the shared label_570c21, 0xb).
|
||||
0x1000u => ownSend ? 0x0Bu : 0x0Au, // Patron
|
||||
0x2000u => ownSend ? 0x0Bu : 0x0Au, // Vassal
|
||||
0x4000u => ownSend ? 0x0Bu : 0x0Au, // Follower / Monarch
|
||||
0x1000u or 0x2000u or 0x4000u =>
|
||||
ownSend ? (uint)RetailLogTextType.SocialSend : (uint)RetailLogTextType.Social,
|
||||
|
||||
// Co-Vassals / Allegiance Broadcast: same type for hear and send.
|
||||
// pc:00571025/00571014 (hear, 0xa) / pc:00570e17/00570df7 (send, 0xa).
|
||||
0x1000000u => 0x0Au, // Co-Vassals
|
||||
0x2000000u => 0x0Au, // Allegiance Broadcast
|
||||
0x1000000u or 0x2000000u => (uint)RetailLogTextType.Social,
|
||||
|
||||
// Unnamed bit — no producer traced, but retail's own dispatch
|
||||
// assigns it 0x13 (Fellowship's slot) for both hear and send.
|
||||
// pc:00570d43 (send, 0x13); the hear branch mirrors it via the
|
||||
// same ebp==0x4000000 special case one level up in the dispatch.
|
||||
0x4000000u => 0x13u,
|
||||
// FellowBroadcast: the sbb idiom's ebp==0x4000000 special case picks
|
||||
// Fellowship's OWN-SEND slot (0x13) but the plain sbb Channel value
|
||||
// (0x08) for hear — pc:00570d43 (send, 0x13). This is the one
|
||||
// unnamed bit that behaves like neither a pure catch-all nor a pure
|
||||
// Fellowship alias.
|
||||
0x4000000u =>
|
||||
ownSend ? (uint)RetailLogTextType.Fellowship : (uint)RetailLogTextType.Channel,
|
||||
|
||||
// The single named non-family bit inside the generic catch-all:
|
||||
// 0x400 gets its OWN color (Help, 0xF) where every other
|
||||
// unmatched bit gets the catch-all (Abuse, 0xE). Both branches
|
||||
// (hear label_570f0a and send label_570d4f) test `ebp != 0x400`
|
||||
// with the identical 0xe/0xf split.
|
||||
0x0400u => 0x0Fu,
|
||||
|
||||
// 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,
|
||||
// Generic catch-all: the sbb-derived Channel (hear, 0x08) /
|
||||
// Channel_Send (own-send, 0x09) pair — Admin, Audit, Advocate, QA,
|
||||
// Sentinel, town channels, and every other unmatched bit. This is
|
||||
// the branch Binary Ninja's `esi - esi` mis-rendering hid; the real
|
||||
// masks are decoded from the raw bytes at VA 0x00570F0A (hear,
|
||||
// mask -6 -> 0x08) and VA 0x00570D4F (send, mask -5 -> 0x09).
|
||||
_ => ownSend ? (uint)RetailLogTextType.ChannelSend : (uint)RetailLogTextType.Channel,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
||||
// Client-local text (a bot script's own injected message, never
|
||||
// reaches the wire), 0x1A — same category as ChatVM.ShowSystemMessage.
|
||||
// Informational sink — 0x00 Default, NOT 0x1A (corrected 2026-08-09,
|
||||
// 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) =>
|
||||
RequireRuntime().CommunicationOwner.Chat.OnSystemMessage(
|
||||
message,
|
||||
chatType: 0x1Au);
|
||||
chatType: 0x00u);
|
||||
|
||||
public void IncrementBusy() =>
|
||||
RequireRuntime().ActionOwner.Transactions
|
||||
|
|
|
|||
|
|
@ -113,12 +113,16 @@ public sealed class ChatVM : IDisposable
|
|||
/// local feedback without round-tripping the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LogTextType <c>0x1A</c>: this text never reaches the wire — the
|
||||
/// same "client-local text" category as retail's own command-parser
|
||||
/// errors and <c>cant_jump_*</c> strings (research doc
|
||||
/// <c>docs/research/2026-08-09-chat-retail-color-table.md</c> §2.2).
|
||||
/// LogTextType <c>0x00</c> Default, NOT <c>0x1A</c> (corrected 2026-08-09,
|
||||
/// Opus review of 172c6f9a). This sink is <c>ClientCommandController</c>'s
|
||||
/// general-purpose output — @version, /loc, friends list, usage lines —
|
||||
/// 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>
|
||||
public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x1Au);
|
||||
public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x00u);
|
||||
|
||||
/// <summary>
|
||||
/// Drain the chat log. Used by the /clear client-side command.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue