The new configurable Mud Engine Server Adapter
Back in May I posted about how you could configure the mud server for use with the Mud Engine. Since then, I have built out an implementation of the Adapter pattern. With that, I wanted to demonstrate how you would startup the server with the engine, and configure it.
Previously in the Mud Engine
In the last iteration of my configuration setup, you had to implement the abstract ServerBootstrap
class, and then build out an implementation of IServerConfiguration
. The bootstrap implementation looked something like this.
public class DefaultServerBootstrap : ServerBootstrap
{
protected override void Run()
{
this.Server.GetCurrentGame()
.NotificationCenter
.Subscribe<ServerMessage>((msg, sub) =>
Console.Write(msg.Content));
while (this.Server.Status != ServerStatus.Stopped)
{
Thread.Sleep(100);
}
}
protected override void RegisterAssemblies()
{
base.RegisterAssemblies();
}
protected override void ConfigureServices()
{
}
protected override IServerConfiguration CreateServerConfiguration()
{
return new ServerConfiguration();
}
protected override IServer CreateServer()
{
var server = new Server();
server.PlayerConnected += this.ExecuteInitialCommand;
return server;
}
protected override IGame CreateGame()
{
return new DefaultGame();
}
protected override IInputCommand InitialConnectionCommand()
{
returnnew PlayerLoginCommand();
}
protected override void RegisterAllowedSecurityRoles(IEnumerable<ISecurityRole> roles)
{
}
}
Then in the application startup, you had to setup the bootstrap.
public static void Main(string[] args)
{
var bootstrap = new DefaultServerBootstrap();
Task bootstrapTask = bootstrap.Initialize();
bootstrapTask.Wait();
}
This was the minimum that had to be done in order to start the game with a server. There were several issues with this approach, one of which is violating SRP. The bootstrap does a lot of different things that aren't even related to the server. It defines the initial command, creates a player, and creates the game. None of which are specific to the server. These actions can be applicable in a single-player game as well.
The other thing that the bootstrap does is configures any services, and register security roles with the services. Gross. The server startup should be free of IoC dependency injection setup goo. So I set out to fix that with the new Mud Engine Adapters.
Setting up a server with adapters
Now that the server has been migrated over to use the adapter architecture, it's much more straight-forward to get a server running. Previously, the server owned the game. Now, the game owns the server - it just doesn't know it.
To start up the game, and give it a server, we do this.
static void Main(string[] args)
{
var serverConfig = new ServerConfiguration();
IServer server = new WindowsServer(new TestPlayerFactory(), new ConnectionFactory(), serverConfig);
var gameConfig = new GameConfiguration();
gameConfig.UseAdapter(server);
var game = new MudGame();
game.Configure(gameConfig);
Task gameStartTask = game.StartAsync();
gameStartTask.Wait();
}
In the code above, we create a serverconfiguration
. Then we create a WindowsServer
instance, giving it a couple of factories and our config. Next we we create a GameConfiguration
and register our WindowsServer
with the game config. When we call game.Configure(gameConfig)
, the game will consume the WindowsServer
adapter and automatically configure the server using the ServerConfiguration
.
Lastly, we start the game calling game.StartAsync()
. This initialize and run all of the registered adapters that the game has. In our case, it initializes and runs the WindowsServer
. At this point, the WindowsServer
is running and can accept incoming client connections.
There is some flexability when it comes to configuring adapters. Each adapter can either skip configuration all-together, or opt into configuration by requiring a configuration class. This can be any class that implements the IConfiguration
interface. Taking this approach allows each adapter to have their own config that can be tailored to what they want to expose. In the case of Server Configuration
, there is quiet a bit we can do with it. For instance:
var serverConfig = new ServerConfiguration();
serverConfig.OnServerStartup = (context) =>
Console.WriteLine($"Server running on port {context.Server.RunningPort}");
That code registers a callback that will be invoked when the server is starting. In this example, we just lookup the port the server is running on and write it out to the console. You can do more then just that however.
static void Main(string[] args)
{
var serverConfig = new ServerConfiguration();
serverConfig.OnServerStartup = (context) =>
{
context.ListeningSocket.BeginAccept(
new AsyncCallback(Program.HandleClientConnection), context.ListeningSocket);
context.SetServerState(ServerStatus.Running);
context.IsHandled = true;
};
IServer server = new WindowsServer(new TestPlayerFactory(), new ConnectionFactory(), serverConfig);
var gameConfig = new GameConfiguration();
gameConfig.UseAdapter(server);
var game = new MudGame();
game.Configure(gameConfig);
Task gameStartTask = game.StartAsync();
gameStartTask.Wait();
}
private static void HandleClientConnection(IAsyncResult result)
{
Socket server = (Socket)result.AsyncState;
Socket clientConnection = server.EndAccept(result);
// Fetch the next incoming connection.
server.BeginAccept(new AsyncCallback(HandleClientConnection), server);
// Create player character here.
}
In this example, we used both the WindowsServer
and it's Socket
to completely replace the way incoming connections are handled. We're not replacing how client communication between the server and client are handled (that is planned) but allowing users to define custom server behavior without reimplementing IServer
. At the end of the OnServerStartup
callback, we set IsHandled
to true, which tells the server to stop using its internal startup and rely on the custom logic being defined.
There are more things you can do, such as intercepting the shutdown of the server, client connections, disconnects and more.
Sharing information across adapters
The server publishes out messages that you can intercept and react to as well. An example is this:
static void Main(string[] args)
{
SetupMessageBrokering();
var serverConfig = new ServerConfiguration();
IServer server = new WindowsServer(new TestPlayerFactory(), new ConnectionFactory(), serverConfig);
var gameConfig = new GameConfiguration();
gameConfig.UseAdapter(server);
var game = new MudGame();
game.Configure(gameConfig);
Task gameStartTask = game.StartAsync();
gameStartTask.Wait();
}
static void SetupMessageBrokering()
{
MessageBrokerFactory.Instance.Subscribe<InfoMessage>(
(msg, subscription) => Console.WriteLine(msg.Content));
MessageBrokerFactory.Instance.Subscribe<GameMessage>(
(msg, subscription) => Console.WriteLine(msg.Content));
MessageBrokerFactory.Instance.Subscribe<PlayerCreatedMessage>(
(msg, sub) => Console.WriteLine("Player connected."));
MessageBrokerFactory.Instance.Subscribe<PlayerDeletionMessage>(
(msg, sub) =>
{
Console.WriteLine("Player disconnected.");
});
}
The first thing we do is subscribe, via the MessageBroker
, to a series of messages that can be published by other objects. In this scenario, the WindowsServer
will publish messages that are InfoMessage
, PlayerCreatedMessage
and PlayerDeletionMessage
. The MudGame
will publish GameMessage
notifications. This looks like this when the server is running and has accepted several connections, and had a few disconnects.
The flexability given now with adapters and their custom configurators will make it easy to get up and running, and still provide you with a lot of flexability when you want to do more advanced things.
As always you can replace the IServer
or IServerConfiguration
implementations with your own all together. Since IServer
inherits from IAdapter
your custom implementation can plug straight into the game using the game.UseAdapter(customServer)
call.
Handling what is missing
The original implementation shown at the start of this post had some other things being done. It handled the setup of security and commanding. That responsibility will be moved to additional adapters, which will run independent of the server and game. They can subscribe to messages sent from the server and react to them, launching the initial login command and handling security without knowing that the server even exists.
var gameConfig = new GameConfiguration();
gameConfig.UseAdapter(new WindowsServer());
gameConfig.UseAdapter(new CommandManager());
gameConfig.UseAdapter(new SecurityBroker());
gameConfig.UseAdapter(new WorldManager());
var game = new MudGame();
game.Configure(gameConfig);
Task gameStartTask = game.StartAsync();
Each one of the adapters shown above have not been written yet; they are coming. When they are ready, they will share the same level of flexability as the server, using configuration objects that you can intercept and interact with.
There is a lot of other new stuff happening in the engine that won't fit into this post, like a new home on GitHub (more on that in a different post), character APIs and real usage documentation.
More coming soon!