Chat on a server
BSGameLib.Server creates a chat engine for you, registers your commands and starts them before it
listens. This page covers what the server adds; the engine itself, commands, tokens and events are in
Chat.
The engine
The server provides an IChatEngine<T> for your player type. Inject it as
IChatEngine<Player>. It comes in one of two ways:
- Without an engine of your own, the server uses a default. It runs commands with
/, and stores every other line in IChatProvider on theGLOBALchannel, as<name>: <text>. The name is the sender'sNamewhen the sender implements INameable and the name is not blank, andUnknownotherwise. So a player type that implementsINameableis named in the chat; one that does not isUnknown. An engine of your own can name the sender in any other way. - To use your own, derive from
BaseChatEngine<TSource>, in the server's project or in any library it scans (see How it finds your code). Your engine serves its own source type; any other source type still gets the default.
The server calls each engine's Initialize() before it starts listening (the default engine's when
it is made), so the commands your ICommandProvider types name are loaded without you calling it.
If your engine overrides Initialize() to do work of its own, such as registering replacement
strings, do that work only on the first call: your own code may call it too, and registering the
same replacement twice throws.
The server doesn't route chat packets for you. Your packet handler passes each line to the engine:
using System.Collections.Concurrent;
using BSGameLib.Chat;
using BSGameLib.Core;
using BSGameLib.Networking;
using BSGameLib.Server;
public class Player : IMessageSource, INameable
{
public Guid UniqueId { get; init; }
public string Name { get; set; } = "";
}
public class PlayerRegistry : IInjectedDependency
{
private readonly ConcurrentDictionary<Guid, Player> _players = new();
public void Add(Player player) => _players[player.UniqueId] = player;
public bool TryGet(Guid sessionId, out Player player) => _players.TryGetValue(sessionId, out player);
}
public class ChatPacket : IDataPacket
{
public string Message { get; set; } = "";
}
public class ChatPacketHandler(
IChatEngine<Player> chat,
IConnectionRegistry<Guid> sessions,
PlayerRegistry players) : IDataPacketHandler<ChatPacket>
{
public void Handle(INetConnection connection, ChatPacket packet)
{
if (!sessions.TryGetSessionId(connection, out var sessionId))
return;
if (!players.TryGet(sessionId, out var player))
return;
chat.Handle(player, packet.Message);
}
}
A custom engine is found by the server like any other class, and its constructor takes what it needs from dependency injection:
using BSGameLib.Chat;
using BSGameLib.Server;
public sealed class GameChat(IEnumerable<ICommandProvider> providers, ICommandFactoryProvider factories)
: BaseChatEngine<Player>(providers, factories)
{
public override void Handle(Player source, string message)
{
if (TryHandleCommand(source, ref message))
return;
Console.WriteLine($"{source.Name}: {message}");
}
}
public class Player : IMessageSource
{
public string Name { get; set; } = "";
}
Setting up commands
Register commands in an IInitializer, not in IGameServer.Start: a client can send a chat line
before Start returns (see The game loop):
using BSGameLib.Chat;
using BSGameLib.Chat.Attributes;
using BSGameLib.Server;
public class ChatSetup(IChatEngine<Player> chat) : IInitializer
{
public void Initialize()
{
chat.CreateCommand("who")
.WithKeywords("who")
.WithAction(ctx => Console.WriteLine($"{ctx.SourceTo<Player>().Name} asked who"))
.Build();
chat.RegisterCommand<GiveCommand>();
}
}
[Command("give", "g")]
public class GiveCommand : ICommand
{
[RequiredParameter(1, "item")]
public string Item { get; set; } = "";
public void Execute(ICommandContext context) => Console.WriteLine($"Give {Item}");
}
public class Player : IMessageSource
{
public string Name { get; set; } = "";
}
A command class can still have a factory of its own. Register an ICommandFactory<TCommand> for it,
and the server uses that instead of building the command with dependency injection.
Recent chat
IChatProvider holds the lines the default engine stores, in order, and keeps
the most recent 10,000. GetByChannel returns the lines on one channel, and GetByDateRange the
lines between two times. The lines are kept for the server's own use: nothing sends them to a
player.