feat: add world chat UI
This commit is contained in:
@@ -86,6 +86,27 @@ namespace XWorld.Framework.Tests
|
||||
Assert.Equal("version mismatch", d.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorldChatMessages_RoundTrip()
|
||||
{
|
||||
var send = new WorldChatSendMsg { Text = "hello world" };
|
||||
WorldChatSendMsg decodedSend = WorldChatSendMsg.Decode(send.Encode());
|
||||
Assert.Equal("hello world", decodedSend.Text);
|
||||
|
||||
var broadcast = new WorldChatMessageMsg
|
||||
{
|
||||
PlayerId = 42,
|
||||
PlayerName = "Alice",
|
||||
Text = "hello everyone",
|
||||
ServerTimeMs = 123456789L
|
||||
};
|
||||
WorldChatMessageMsg decodedBroadcast = WorldChatMessageMsg.Decode(broadcast.Encode());
|
||||
Assert.Equal(42, decodedBroadcast.PlayerId);
|
||||
Assert.Equal("Alice", decodedBroadcast.PlayerName);
|
||||
Assert.Equal("hello everyone", decodedBroadcast.Text);
|
||||
Assert.Equal(123456789L, decodedBroadcast.ServerTimeMs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Opcodes_AreStableValues()
|
||||
{
|
||||
@@ -97,6 +118,8 @@ namespace XWorld.Framework.Tests
|
||||
Assert.Equal((ushort)5, (ushort)FrameworkOpcode.Snapshot);
|
||||
Assert.Equal((ushort)6, (ushort)FrameworkOpcode.RoomEnd);
|
||||
Assert.Equal((ushort)7, (ushort)FrameworkOpcode.Error);
|
||||
Assert.Equal((ushort)16, (ushort)FrameworkOpcode.WorldChatSend);
|
||||
Assert.Equal((ushort)17, (ushort)FrameworkOpcode.WorldChatMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace XWorld.Server.Gateway.Tests
|
||||
Moving = moving
|
||||
}.Encode());
|
||||
|
||||
private static Frame WorldChatFrame(string text)
|
||||
=> new Frame(Channel.Framework, (ushort)FrameworkOpcode.WorldChatSend,
|
||||
new WorldChatSendMsg { Text = text }.Encode());
|
||||
|
||||
private static List<Frame> Frames(FakeConnection c)
|
||||
{
|
||||
var list = new List<Frame>();
|
||||
@@ -85,6 +89,35 @@ namespace XWorld.Server.Gateway.Tests
|
||||
Assert.True(decoded.Moving);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorldChat_IsBroadcastToAllConnectedPlayers()
|
||||
{
|
||||
var loop = NewLoop();
|
||||
var c1 = new FakeConnection(1);
|
||||
var c2 = new FakeConnection(2);
|
||||
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1 });
|
||||
loop.Submit(new ConnectCommand { PlayerId = 2, Connection = c2 });
|
||||
loop.Submit(new FrameCommand { PlayerId = 1, Frame = LobbyJoinFrame("Alice") });
|
||||
loop.Submit(new FrameCommand { PlayerId = 2, Frame = LobbyJoinFrame("Bob") });
|
||||
loop.DrainAndTick(0f);
|
||||
|
||||
c1.Sent.Clear();
|
||||
c2.Sent.Clear();
|
||||
loop.Submit(new FrameCommand { PlayerId = 1, Frame = WorldChatFrame("hello world") });
|
||||
loop.DrainAndTick(0f);
|
||||
|
||||
Frame selfFrame = Frames(c1).Find(f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.WorldChatMessage);
|
||||
Frame otherFrame = Frames(c2).Find(f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.WorldChatMessage);
|
||||
Assert.NotEqual(default, selfFrame);
|
||||
Assert.NotEqual(default, otherFrame);
|
||||
|
||||
WorldChatMessageMsg message = WorldChatMessageMsg.Decode(otherFrame.Payload);
|
||||
Assert.Equal(1, message.PlayerId);
|
||||
Assert.Equal("Alice", message.PlayerName);
|
||||
Assert.Equal("hello world", message.Text);
|
||||
Assert.True(message.ServerTimeMs > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reconnect_ReroutesRoomOutputToNewConnection()
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace XWorld.Server.Gateway
|
||||
private readonly Matchmaker _matchmaker;
|
||||
private readonly ChannelsNS.Channel<ServerCommand> _cmds =
|
||||
ChannelsNS.Channel.CreateUnbounded<ServerCommand>();
|
||||
private const int MaxWorldChatTextLength = 200;
|
||||
private readonly Dictionary<string, List<int>> _roomPlayers = new Dictionary<string, List<int>>();
|
||||
private readonly Dictionary<int, LobbyPlayerMsg> _lobbyPlayers = new Dictionary<int, LobbyPlayerMsg>();
|
||||
private readonly Dictionary<string, int> _playerCountCache = new Dictionary<string, int>(); // "gameId@version" -> playerCount
|
||||
@@ -156,6 +157,9 @@ namespace XWorld.Server.Gateway
|
||||
case FrameworkOpcode.LobbyLeave:
|
||||
if (_lobbyPlayers.Remove(pid)) BroadcastLobbyState();
|
||||
break;
|
||||
case FrameworkOpcode.WorldChatSend:
|
||||
HandleWorldChatSend(pid, frame.Payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +217,41 @@ namespace XWorld.Server.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleWorldChatSend(int pid, byte[] payload)
|
||||
{
|
||||
if (!_sessions.IsConnected(pid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var req = WorldChatSendMsg.Decode(payload);
|
||||
string text = (req.Text ?? string.Empty).Trim();
|
||||
if (text.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (text.Length > MaxWorldChatTextLength)
|
||||
{
|
||||
text = text.Substring(0, MaxWorldChatTextLength);
|
||||
}
|
||||
|
||||
string name = _lobbyPlayers.TryGetValue(pid, out var player) && !string.IsNullOrWhiteSpace(player.Name)
|
||||
? player.Name
|
||||
: $"P{pid}";
|
||||
var msg = new WorldChatMessageMsg
|
||||
{
|
||||
PlayerId = pid,
|
||||
PlayerName = name,
|
||||
Text = text,
|
||||
ServerTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
byte[] encoded = msg.Encode();
|
||||
foreach (int targetPid in _sessions.ConnectedPlayerIds())
|
||||
{
|
||||
SendFramework(targetPid, FrameworkOpcode.WorldChatMessage, encoded);
|
||||
}
|
||||
}
|
||||
|
||||
private void BroadcastLobbyState()
|
||||
{
|
||||
if (_lobbyPlayers.Count == 0) return;
|
||||
|
||||
@@ -38,6 +38,19 @@ namespace XWorld.Server.Gateway
|
||||
|
||||
public Session Get(int pid) => _byPid.TryGetValue(pid, out var s) ? s : null;
|
||||
|
||||
public IReadOnlyList<int> ConnectedPlayerIds()
|
||||
{
|
||||
List<int> players = null;
|
||||
foreach (var kv in _byPid)
|
||||
{
|
||||
if (kv.Value.Connected)
|
||||
{
|
||||
(players ??= new List<int>()).Add(kv.Key);
|
||||
}
|
||||
}
|
||||
return players ?? (IReadOnlyList<int>)Array.Empty<int>();
|
||||
}
|
||||
|
||||
/// <summary>移除断线且"严格越过"重连窗口的会话(currentTick - DisconnectedAtTick > windowTicks)。返回被移除的会话;无则空数组。</summary>
|
||||
public IReadOnlyList<Session> SweepExpired(long currentTick, long windowTicks)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user