diff --git a/Nibriboard/CommandConsole.cs b/Nibriboard/CommandConsole.cs
new file mode 100644
index 0000000..564421b
--- /dev/null
+++ b/Nibriboard/CommandConsole.cs
@@ -0,0 +1,99 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Nibriboard.RippleSpace;
+
+namespace Nibriboard
+{
+ public class CommandConsole
+ {
+ private NibriboardServer server;
+ private TcpListener commandServer;
+
+ private int commandPort;
+
+ public CommandConsole(NibriboardServer inServer, int inCommandPort)
+ {
+ server = inServer;
+ commandPort = inCommandPort;
+ }
+
+ public async Task Start()
+ {
+ commandServer = new TcpListener(IPAddress.IPv6Loopback, server.CommandPort);
+ commandServer.Start();
+ Log.WriteLine("[CommandConsole] Listening on {0}.", new IPEndPoint(IPAddress.IPv6Loopback, server.CommandPort));
+ while(true)
+ {
+ TcpClient nextClient = await commandServer.AcceptTcpClientAsync();
+
+ StreamReader source = new StreamReader(nextClient.GetStream());
+ StreamWriter destination = new StreamWriter(nextClient.GetStream()) { AutoFlush = true };
+
+ string rawCommand = await source.ReadLineAsync();
+ string[] commandParts = rawCommand.Split(" \t".ToCharArray());
+ Log.WriteLine("[CommandConsole] Client executing {0}", rawCommand);
+
+ try
+ {
+ await executeCommand(destination, commandParts);
+ }
+ catch(Exception error)
+ {
+ try
+ {
+ await destination.WriteLineAsync(error.ToString());
+ }
+ catch { nextClient.Close(); } // Make absolutely sure that the command server won't die
+ }
+ nextClient.Close();
+ }
+ }
+
+ private async Task executeCommand(StreamWriter destination, string[] commandParts)
+ {
+ string commandName = commandParts[0].Trim();
+ switch(commandName)
+ {
+ case "help":
+ await destination.WriteLineAsync("Nibriboard Server Command Console");
+ await destination.WriteLineAsync("=================================");
+ await destination.WriteLineAsync("Available commands:");
+ await destination.WriteLineAsync(" help Show this message");
+ await destination.WriteLineAsync(" save Save the ripplespace to disk");
+ await destination.WriteLineAsync(" planes List all the currently loaded planes");
+ break;
+ case "save":
+ await destination.WriteAsync("Saving ripple space - ");
+ await server.PlaneManager.Save();
+ await destination.WriteLineAsync("done.");
+ await destination.WriteLineAsync($"Save is now {BytesToString(server.PlaneManager.LastSaveFileSize)} in size.");
+ break;
+ case "planes":
+ await destination.WriteLineAsync("Planes:");
+ foreach(Plane plane in server.PlaneManager.Planes)
+ await destination.WriteLineAsync($" {plane.Name} @ {plane.ChunkSize} ({plane.LoadedChunks} / ~{plane.SoftLoadedChunkLimit} chunks loaded, {plane.UnloadableChunks} inactive)");
+ await destination.WriteLineAsync();
+ await destination.WriteLineAsync($"Total {server.PlaneManager.Planes.Count}");
+ break;
+
+ default:
+ await destination.WriteLineAsync($"Error: Unrecognised command {commandName}");
+ break;
+ }
+ }
+
+ public static string BytesToString(long byteCount)
+ {
+ string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; // Longs run out around EB
+ if(byteCount == 0)
+ return "0" + suf[0];
+ long bytes = Math.Abs(byteCount);
+ int place = (int)Math.Floor(Math.Log(bytes, 1024));
+ double num = Math.Round(bytes / Math.Pow(1024, place), 1);
+ return (Math.Sign(byteCount) * num).ToString() + suf[place];
+ }
+ }
+}
diff --git a/Nibriboard/Nibriboard.csproj b/Nibriboard/Nibriboard.csproj
index 602a42f..44850ee 100644
--- a/Nibriboard/Nibriboard.csproj
+++ b/Nibriboard/Nibriboard.csproj
@@ -143,6 +143,7 @@
+