Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions MultiAdmin/EventInterfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public interface IEventServerFull : IMAEvent
void OnServerFull();
}

public interface IEventIdleEnter : IMAEvent
{
void OnIdleEnter();
}

public interface IEventIdleExit : IMAEvent
{
void OnIdleExit();
}

public interface ICommand
{
void OnCall(string[] args);
Expand Down
5 changes: 4 additions & 1 deletion MultiAdmin/ModFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ public enum ModFeatures
// Replaces detecting game output with MultiAdmin events for game events
CustomEvents = 1 << 0,

// Uses a custom restart command ("RESTART")
RestartCommand = 2 << 0,

// Supporting all current features
All = ~(~0 << 1)
All = ~(~0 << 2)
}
}
27 changes: 20 additions & 7 deletions MultiAdmin/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,9 @@ public void StartServer(bool restartOnCrash = true)

// Start the output reader
OutputHandler outputHandler = new OutputHandler(this);
// Assign the socket events to the OutputHandler
consoleSocket.OnReceiveMessage += outputHandler.HandleMessage;
consoleSocket.OnReceiveAction += outputHandler.HandleAction;

// Finally, start the game
GameProcess = Process.Start(startInfo);
Expand Down Expand Up @@ -426,6 +428,10 @@ public void StartServer(bool restartOnCrash = true)

consoleSocket.Disconnect();

// Remove the socket events for OutputHandler
consoleSocket.OnReceiveMessage -= outputHandler.HandleMessage;
consoleSocket.OnReceiveAction -= outputHandler.HandleAction;

SessionSocket = null;
StartDateTime = null;
}
Expand Down Expand Up @@ -476,22 +482,29 @@ public void StopServer(bool killGame = false)

ForEachHandler<IEventServerStop>(stopEvent => stopEvent.OnServerStop());

if (killGame && IsGameProcessRunning)
GameProcess.Kill();
else if (!SendMessage("QUIT"))
if ((killGame || !SendMessage("QUIT")) && IsGameProcessRunning)
GameProcess.Kill();
}

public void SoftRestartServer()
public void SoftRestartServer(bool killGame = false)
{
if (!IsRunning) throw new Exceptions.ServerNotRunningException();

initRestartTimeoutTime = DateTime.Now;
Status = ServerStatus.Restarting;

SendMessage("ROUNDRESTART");
if (!SendMessage("QUIT"))
GameProcess.Kill();
if (supportedModFeatures.HasFlag(ModFeatures.RestartCommand))
{
SendMessage("RESTART");
if (killGame && IsGameProcessRunning)
GameProcess.Kill();
}
else
{
SendMessage("ROUNDRESTART");
if ((killGame || !SendMessage("QUIT")) && IsGameProcessRunning)
GameProcess.Kill();
}
}

#endregion
Expand Down
66 changes: 53 additions & 13 deletions MultiAdmin/ServerIO/OutputHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using MultiAdmin.ConsoleTools;
using MultiAdmin.Utility;
Expand All @@ -15,27 +14,35 @@ public class OutputHandler

private readonly Server server;

private enum OutputCodes : byte
{
//0x00 - 0x0F - reserved for colors

RoundRestart = 0x10,
IdleEnter = 0x11,
IdleExit = 0x12,
ExitActionReset = 0x13,
ExitActionShutdown = 0x14,
ExitActionSilentShutdown = 0x15,
ExitActionRestart = 0x16
}

public OutputHandler(Server server)
{
this.server = server;
}

public void HandleMessage(object source, string message)
public void HandleMessage(object source, ServerSocket.MessageEventArgs message)
{
if (message == null)
if (message.message == null)
return;

ColoredMessage coloredMessage = new ColoredMessage(message, ConsoleColor.White);
ColoredMessage coloredMessage = new ColoredMessage(message.message, ConsoleColor.White);

if (!coloredMessage.text.IsEmpty())
{
// Convert the first character to the corresponding color
if (byte.TryParse(coloredMessage.text[0].ToString(), NumberStyles.HexNumber,
NumberFormatInfo.CurrentInfo, out byte consoleColor))
{
coloredMessage.textColor = (ConsoleColor)consoleColor;
coloredMessage.text = coloredMessage.text.Substring(1);
}
// Parse the color byte
coloredMessage.textColor = (ConsoleColor)message.color;

// Smod2 loggers pretty printing
Match match = SmodRegex.Match(coloredMessage.text);
Expand Down Expand Up @@ -91,11 +98,12 @@ public void HandleMessage(object source, string message)
server.ForEachHandler<IEventRoundEnd>(roundEnd => roundEnd.OnRoundEnd());
break;

/* Replaced by OutputCodes.RoundRestart
case "waiting for players":
server.IsLoading = false;

server.ForEachHandler<IEventWaitingForPlayers>(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers());
break;
*/

case "new round has been started":
server.ForEachHandler<IEventRoundStart>(roundStart => roundStart.OnRoundStart());
Expand Down Expand Up @@ -128,11 +136,12 @@ public void HandleMessage(object source, string message)
server.ForEachHandler<IEventRoundEnd>(roundEnd => roundEnd.OnRoundEnd());
break;

/* Replaced by OutputCodes.RoundRestart
case "waiting-for-players-event":
server.IsLoading = false;

server.ForEachHandler<IEventWaitingForPlayers>(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers());
break;
*/

case "round-start-event":
server.ForEachHandler<IEventRoundStart>(roundStart => roundStart.OnRoundStart());
Expand Down Expand Up @@ -161,5 +170,36 @@ public void HandleMessage(object source, string message)

server.Write(coloredMessage);
}

public void HandleAction(object source, byte action)
{
switch ((OutputCodes)action)
{
// This seems to show up at the waiting for players event
case OutputCodes.RoundRestart:
server.IsLoading = false;
server.ForEachHandler<IEventWaitingForPlayers>(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers());
break;

case OutputCodes.IdleEnter:
server.ForEachHandler<IEventIdleEnter>(idleEnter => idleEnter.OnIdleEnter());
break;

case OutputCodes.IdleExit:
server.ForEachHandler<IEventIdleExit>(idleExit => idleExit.OnIdleExit());
break;

// Unhandled for now
case OutputCodes.ExitActionReset:
case OutputCodes.ExitActionShutdown:
case OutputCodes.ExitActionSilentShutdown:
case OutputCodes.ExitActionRestart:
break;

default:
Program.LogDebug(nameof(HandleAction), $"Received unknown output code ({action}), is MultiAdmin up to date? This error can probably be safely ignored.");
break;
}
}
}
}
49 changes: 42 additions & 7 deletions MultiAdmin/ServerIO/ServerSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MultiAdmin.ConsoleTools;

namespace MultiAdmin.ServerIO
{
Expand All @@ -21,7 +22,20 @@ public class ServerSocket : IDisposable
private TcpClient client;
private NetworkStream networkStream;

public event EventHandler<string> OnReceiveMessage;
public struct MessageEventArgs
{
public MessageEventArgs(string message, byte color)
{
this.message = message;
this.color = color;
}

public readonly string message;
public readonly byte color;
}

public event EventHandler<MessageEventArgs> OnReceiveMessage;
public event EventHandler<byte> OnReceiveAction;

public int Port => ((IPEndPoint)listener.LocalEndpoint).Port;

Expand Down Expand Up @@ -61,31 +75,52 @@ public void Connect()

public async void MessageListener()
{
byte[] typeBuffer = new byte[1];
byte[] intBuffer = new byte[IntBytes];
while (!disposed && networkStream != null)
{
try
{
int messageTypeBytesRead =
await networkStream.ReadAsync(typeBuffer, 0, 1, disposeCancellationSource.Token);

// Socket has been disconnected
if (messageTypeBytesRead <= 0)
{
Disconnect();
break;
}

byte messageType = typeBuffer[0];

// 16 colors reserved, otherwise process as control message (action)
if (messageType >= 16)
{
OnReceiveAction?.Invoke(this, messageType);
continue;
}

int lengthBytesRead =
await networkStream.ReadAsync(intBuffer, 0, IntBytes, disposeCancellationSource.Token);

// Socket has been disconnected
if (lengthBytesRead <= 0)
// Socket has been disconnected or integer read is invalid
if (lengthBytesRead != IntBytes)
{
Disconnect();
break;
}

int length = BitConverter.ToInt32(intBuffer, 0);
// Decode integer
int length = (intBuffer[0] << 24) | (intBuffer[1] << 16) | (intBuffer[2] << 8) | intBuffer[3];

// Handle empty messages asap
if (length == 0)
{
OnReceiveMessage?.Invoke(this, "");
OnReceiveMessage?.Invoke(this, new MessageEventArgs("", messageType));
}
else if (length < 0)
{
OnReceiveMessage?.Invoke(this, null);
OnReceiveMessage?.Invoke(this, new MessageEventArgs(null, messageType));
}

byte[] messageBuffer = new byte[length];
Expand All @@ -101,7 +136,7 @@ public async void MessageListener()

string message = Encoding.GetString(messageBuffer, 0, length);

OnReceiveMessage?.Invoke(this, message);
OnReceiveMessage?.Invoke(this, new MessageEventArgs(message, messageType));
}
catch (Exception e)
{
Expand Down