feat(ui+net): #16 LiveCommandBus + WorldSession.Send{Talk,Tell,Channel} + SendChatCmd wiring
Replaces NullCommandBus.Instance in PanelContext with a real LiveCommandBus when a live session is active. Panels publish SendChatCmd; the host routes it to the right wire opcode + emits a ChatLog.OnSelfSent local echo (optimistic; retail-equivalent for Talk). Pieces: - ChatChannelKind enum (UI.Abstractions) - mirrors holtburger's ChatChannelKind (references/holtburger/.../client/types.rs:35-49). - SendChatCmd record (UI.Abstractions) - (Channel, TargetName?, Text). - LiveCommandBus (UI.Abstractions) - single-handler-per-type; Register<T> throws on double-register; Publish<T> logs missing handler but does not throw. - ChannelResolver (UI.Abstractions) - port of holtburger's resolve_legacy_channel (client/commands.rs:50-62) mapping ChatChannelKind to legacy ChatChannel ids verbatim from holtburger-protocol/.../chat/types.rs:8-24 (Fellow=0x0800, AllegianceBroadcast=0x02000000, Vassals=0x1000, Patron=0x2000, Monarch=0x4000, CoVassals=0x01000000). - WorldSession.SendTalk / SendTell / SendChannel - 3-line wrappers around existing ChatRequests.Build* + SendGameAction. Internal GameActionCapture seam + InternalsVisibleTo for tests. - GameWindow registers SendChatCmd handler: Say -> SendTalk + ChatLog echo, Tell -> SendTell + echo, channel kinds -> ChannelResolver.Resolve -> SendChannel + echo. 12 new tests across SendChatCmd + LiveCommandBus + ChannelResolver + WorldSessionChat. NullCommandBus.Instance retained for back-compat when no live session. Solution total: 893 green (51 + 229 + 613). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ff5ed9ec0b
commit
8e6e5a0b61
10 changed files with 457 additions and 1 deletions
|
|
@ -784,9 +784,27 @@ public sealed class WorldSession : IDisposable
|
|||
/// </summary>
|
||||
public void SendGameAction(byte[] gameActionBody)
|
||||
{
|
||||
// Phase I.3 test seam: when set, intercept the body before the
|
||||
// wire-write path runs (which would otherwise NPE on an unseeded
|
||||
// ISAAC keystream during unit tests). Production callers leave
|
||||
// this null and the body proceeds to the framed/encrypted UDP send.
|
||||
if (GameActionCapture is not null)
|
||||
{
|
||||
GameActionCapture(gameActionBody);
|
||||
return;
|
||||
}
|
||||
SendGameMessage(gameActionBody);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.3: test-only hook. When non-null, <see cref="SendGameAction"/>
|
||||
/// invokes this instead of writing to the wire. Lets unit tests verify
|
||||
/// that <see cref="SendTalk"/>/<see cref="SendTell"/>/<see cref="SendChannel"/>
|
||||
/// produce the bytes they should without standing up a full handshake +
|
||||
/// ISAAC keystream. Production sites never set this.
|
||||
/// </summary>
|
||||
internal Action<byte[]>? GameActionCapture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Phase B.2: get and increment the game-action sequence counter.
|
||||
/// Call once per outbound movement message; pass the returned value
|
||||
|
|
@ -795,6 +813,44 @@ public sealed class WorldSession : IDisposable
|
|||
/// </summary>
|
||||
public uint NextGameActionSequence() => ++_gameActionSequence;
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.3: send a local /say message (heard within ~20m).
|
||||
/// Wraps <see cref="ChatRequests.BuildTalk"/>.
|
||||
/// </summary>
|
||||
public void SendTalk(string text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
uint seq = NextGameActionSequence();
|
||||
byte[] body = ChatRequests.BuildTalk(seq, text);
|
||||
SendGameAction(body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.3: send a /tell (whisper) by target character name.
|
||||
/// Wraps <see cref="ChatRequests.BuildTell"/>.
|
||||
/// </summary>
|
||||
public void SendTell(string targetName, string text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetName);
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
uint seq = NextGameActionSequence();
|
||||
byte[] body = ChatRequests.BuildTell(seq, targetName, text);
|
||||
SendGameAction(body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.3: send to a chat channel (allegiance, fellowship, etc.) by
|
||||
/// the legacy <c>ChatChannel</c> bitflag id.
|
||||
/// Wraps <see cref="ChatRequests.BuildChatChannel"/>.
|
||||
/// </summary>
|
||||
public void SendChannel(uint channelId, string text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
uint seq = NextGameActionSequence();
|
||||
byte[] body = ChatRequests.BuildChatChannel(seq, channelId, text);
|
||||
SendGameAction(body);
|
||||
}
|
||||
|
||||
private void SendGameMessage(byte[] gameMessageBody)
|
||||
{
|
||||
var fragment = GameMessageFragment.BuildSingleFragment(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue