Table of Contents

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 the GLOBAL channel, as <name>: <text>. The name is the sender's Name when the sender implements INameable and the name is not blank, and Unknown otherwise. So a player type that implements INameable is named in the chat; one that does not is Unknown. 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.