Files
RobustToolbox/SS14.Server/BaseServer.cs
Pieter-Jan Briers 153b57ed56 IPv6 & multi-peer NetManager support. (#705)
1. IPv6 support. Woo!
2. NetManager can now explicitly bind to adresses and work with multiple peers. ~~I thought I needed this for IPv6 because I forgot about IPv4-mapped adresses, but the code's written so let's keep it.~~ OpenBSD doesn't support IPv4-mapped IPv6 addresses so effort not wasted.
2018-11-21 20:57:47 +01:00

424 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using SS14.Server.Console;
using SS14.Server.GameStates;
using SS14.Server.Interfaces;
using SS14.Server.Interfaces.Chat;
using SS14.Server.Interfaces.Console;
using SS14.Server.Interfaces.GameObjects;
using SS14.Server.Interfaces.GameState;
using SS14.Server.Interfaces.Placement;
using SS14.Server.Interfaces.Player;
using SS14.Shared;
using SS14.Shared.Configuration;
using SS14.Shared.ContentPack;
using SS14.Shared.GameStates;
using SS14.Shared.Interfaces;
using SS14.Shared.Interfaces.Configuration;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Interfaces.Map;
using SS14.Shared.Interfaces.Network;
using SS14.Shared.Interfaces.Serialization;
using SS14.Shared.Interfaces.Timing;
using SS14.Shared.Interfaces.Timers;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Network;
using SS14.Shared.Network.Messages;
using SS14.Shared.Prototypes;
using SS14.Shared.Map;
using SS14.Server.Interfaces.Maps;
using SS14.Server.Player;
using SS14.Server.ViewVariables;
using SS14.Shared.Asynchronous;
using SS14.Shared.Enums;
using SS14.Shared.Reflection;
using SS14.Shared.Timing;
using SS14.Shared.Utility;
using SS14.Shared.Interfaces.Log;
using SS14.Shared.Interfaces.Resources;
namespace SS14.Server
{
/// <summary>
/// The master class that runs the rest of the engine.
/// </summary>
public class BaseServer : IBaseServer
{
[Dependency]
private readonly ICommandLineArgs _commandLine;
[Dependency]
private readonly IConfigurationManager _config;
[Dependency]
private readonly IComponentManager _components;
[Dependency]
private readonly IServerEntityManager _entities;
[Dependency]
private readonly ILogManager _log;
[Dependency]
private readonly ISS14Serializer _serializer;
[Dependency]
private readonly IGameTiming _time;
[Dependency]
private readonly IResourceManager _resources;
[Dependency]
private readonly IMapManager _mapManager;
[Dependency]
private readonly ITimerManager timerManager;
[Dependency]
private readonly IServerGameStateManager _stateManager;
[Dependency]
private readonly IServerNetManager _network;
[Dependency]
private readonly ISystemConsoleManager _systemConsole;
[Dependency]
private readonly ITaskManager _taskManager;
private FileLogHandler fileLogHandler;
private GameLoop _mainLoop;
private ServerRunLevel _runLevel;
private TimeSpan _lastTitleUpdate;
private int _lastReceivedBytes;
private int _lastSentBytes;
/// <inheritdoc />
public ServerRunLevel RunLevel
{
get => _runLevel;
set => OnRunLevelChanged(value);
}
/// <inheritdoc />
public string MapName => _config.GetCVar<string>("game.mapname");
/// <inheritdoc />
public int MaxPlayers => _config.GetCVar<int>("game.maxplayers");
/// <inheritdoc />
public string ServerName => _config.GetCVar<string>("game.hostname");
/// <inheritdoc />
public string Motd => _config.GetCVar<string>("game.welcomemsg");
/// <inheritdoc />
public string GameModeName { get; set; } = string.Empty;
/// <inheritdoc />
public event EventHandler<RunLevelChangedEventArgs> RunLevelChanged;
/// <inheritdoc />
public void Restart()
{
Logger.Info("[SRV] Restarting Server...");
Cleanup();
Start();
}
/// <inheritdoc />
public void Shutdown(string reason)
{
if (string.IsNullOrWhiteSpace(reason))
Logger.Info("[SRV] Shutting down...");
else
Logger.Info($"[SRV] {reason}, shutting down...");
_mainLoop.Running = false;
_log.RootSawmill.RemoveHandler(fileLogHandler);
fileLogHandler.Dispose();
}
/// <inheritdoc />
public bool Start()
{
//Sets up the configMgr
_config.LoadFromFile(_commandLine.ConfigFile);
//Sets up Logging
_config.RegisterCVar("log.path", "logs", CVar.ARCHIVE);
_config.RegisterCVar("log.format", "log_%(date)s-%(time)s.txt", CVar.ARCHIVE);
_config.RegisterCVar("log.level", LogLevel.Info, CVar.ARCHIVE);
var logPath = _config.GetCVar<string>("log.path");
var logFormat = _config.GetCVar<string>("log.format");
var logFilename = logFormat.Replace("%(date)s", DateTime.Now.ToString("yyyyMMdd")).Replace("%(time)s", DateTime.Now.ToString("hhmmss"));
var fullPath = Path.Combine(logPath, logFilename);
if (!Path.IsPathRooted(fullPath))
{
logPath = PathHelpers.ExecutableRelativeFile(fullPath);
}
fileLogHandler = new FileLogHandler(logPath);
_log.RootSawmill.Level = _config.GetCVar<LogLevel>("log.level");
_log.RootSawmill.AddHandler(fileLogHandler);
// Has to be done early because this guy's in charge of the main thread Synchronization Context.
_taskManager.Initialize();
OnRunLevelChanged(ServerRunLevel.Init);
LoadSettings();
var netMan = IoCManager.Resolve<IServerNetManager>();
try
{
netMan.Initialize(true);
netMan.StartServer();
}
catch (Exception e)
{
var port = netMan.Port;
Logger.Fatal("Unable to setup networking manager. Check port {0} is not already in use and that all binding addresses are correct!\n{1}", port, e);
return true;
}
// Set up the VFS
_resources.Initialize();
#if RELEASE
_resources.MountContentDirectory(@"./Resources/");
#else
// Load from the resources dir in the repo root instead.
// It's a debug build so this is fine.
_resources.MountContentDirectory(@"../../Resources/");
_resources.MountContentDirectory(@"Resources/Assemblies", new ResourcePath("/Assemblies/"));
#endif
//mount the engine content pack
// _resources.MountContentPack(@"EngineContentPack.zip");
//mount the default game ContentPack defined in config
// _resources.MountDefaultContentPack();
//identical code in game controller for client
if (!AssemblyLoader.TryLoadAssembly<GameShared>(_resources, $"Content.Shared"))
{
Logger.Warning($"[ENG] Could not load any Shared DLL.");
}
if (!AssemblyLoader.TryLoadAssembly<GameServer>(_resources, $"Content.Server"))
{
Logger.Warning($"[ENG] Could not load any Server DLL.");
}
// HAS to happen after content gets loaded.
// Else the content types won't be included.
// TODO: solve this properly.
_serializer.Initialize();
// Initialize Tier 2 services
_stateManager.Initialize();
_entities.Initialize();
IoCManager.Resolve<IChatManager>().Initialize();
IoCManager.Resolve<IPlayerManager>().Initialize(MaxPlayers);
_mapManager.Initialize();
IoCManager.Resolve<IPlacementManager>().Initialize();
IoCManager.Resolve<IViewVariablesHost>().Initialize();
// Call Init in game assemblies.
AssemblyLoader.BroadcastRunLevel(AssemblyLoader.RunLevel.Init);
// because of 'reasons' this has to be called after the last assembly is loaded
// otherwise the prototypes will be cleared
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes"));
prototypeManager.Resync();
IoCManager.Resolve<ITileDefinitionManager>().Initialize();
IoCManager.Resolve<IConsoleShell>().Initialize();
IoCManager.Resolve<IConGroupController>().Initialize();
OnRunLevelChanged(ServerRunLevel.PreGame);
return false;
}
/// <inheritdoc />
public void MainLoop()
{
_mainLoop = new GameLoop(_time)
{
SleepMode = SleepMode.Delay
};
_mainLoop.Tick += (sender, args) => Update(args.DeltaSeconds);
// set GameLoop.Running to false to return from this function.
_mainLoop.Run();
Cleanup();
}
/// <summary>
/// Updates the console window title with performance statistics.
/// </summary>
private void UpdateTitle()
{
// every 1 second update stats in the console window title
if ((_time.RealTime - _lastTitleUpdate).TotalSeconds < 1.0)
return;
var netStats = UpdateBps();
System.Console.Title = string.Format("FPS: {0:N2} SD: {1:N2}ms | Net: ({2}) | Memory: {3:N0} KiB",
Math.Round(_time.FramesPerSecondAvg, 2),
_time.RealFrameTimeStdDev.TotalMilliseconds,
netStats,
Process.GetCurrentProcess().PrivateMemorySize64 >> 10);
_lastTitleUpdate = _time.RealTime;
}
/// <summary>
/// Loads the server settings from the ConfigurationManager.
/// </summary>
private void LoadSettings()
{
var cfgMgr = IoCManager.Resolve<IConfigurationManager>();
cfgMgr.RegisterCVar("net.tickrate", 66, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
cfgMgr.RegisterCVar("game.hostname", "MyServer", CVar.ARCHIVE);
cfgMgr.RegisterCVar("game.mapname", "SavedEntities.xml", CVar.ARCHIVE);
cfgMgr.RegisterCVar("game.maxplayers", 32, CVar.ARCHIVE);
cfgMgr.RegisterCVar("game.type", GameType.Game);
cfgMgr.RegisterCVar("game.welcomemsg", "Welcome to the server!", CVar.ARCHIVE);
_time.TickRate = _config.GetCVar<int>("net.tickrate");
Logger.InfoS("srv", $"Name: {ServerName}");
Logger.InfoS("srv", $"TickRate: {_time.TickRate}({_time.TickPeriod.TotalMilliseconds:0.00}ms)");
Logger.InfoS("srv", $"Map: {MapName}");
Logger.InfoS("srv", $"Max players: {MaxPlayers}");
Logger.InfoS("srv", $"Welcome message: {Motd}");
}
/// <summary>
/// Switches the run level of the BaseServer to the desired value.
/// </summary>
private void OnRunLevelChanged(ServerRunLevel level)
{
if (level == _runLevel)
return;
Logger.Debug($"[ENG] Runlevel changed to: {level}");
var args = new RunLevelChangedEventArgs(_runLevel, level);
_runLevel = level;
RunLevelChanged?.Invoke(this, args);
// positive edge triggers
switch (level)
{
case ServerRunLevel.PreGame:
_entities.Startup();
break;
}
}
// called right before main loop returns, do all saving/cleanup in here
private void Cleanup()
{
// shut down networking, kicking all players.
_network.Shutdown("Server Shutdown");
// shutdown entities
_entities.Shutdown();
//TODO: This should prob shutdown all managers in a loop.
// remove all maps
if (_runLevel == ServerRunLevel.Game)
{
var mapMgr = IoCManager.Resolve<IMapManager>();
// TODO: Unregister all maps.
mapMgr.DeleteMap(new MapId(1));
}
}
private string UpdateBps()
{
var stats = IoCManager.Resolve<IServerNetManager>().Statistics;
var bps = $"Send: {(stats.SentBytes - _lastSentBytes) >> 10:N0} KiB/s, Recv: {(stats.ReceivedBytes - _lastReceivedBytes) >> 10:N0} KiB/s";
_lastSentBytes = stats.SentBytes;
_lastReceivedBytes = stats.ReceivedBytes;
return bps;
}
private void Update(float frameTime)
{
UpdateTitle();
_systemConsole.Update();
IoCManager.Resolve<IServerNetManager>().ProcessPackets();
AssemblyLoader.BroadcastUpdate(AssemblyLoader.UpdateLevel.PreEngine, frameTime);
timerManager.UpdateTimers(frameTime);
_taskManager.ProcessPendingTasks();
if (_runLevel >= ServerRunLevel.PreGame)
{
_components.CullRemovedComponents();
_entities.Update(frameTime);
}
AssemblyLoader.BroadcastUpdate(AssemblyLoader.UpdateLevel.PostEngine, frameTime);
_stateManager.SendGameStateUpdate();
}
}
/// <summary>
/// Enumeration of the run levels of the BaseServer.
/// </summary>
public enum ServerRunLevel
{
Error = 0,
Init,
PreGame,
Game,
PostGame,
MapChange,
}
/// <summary>
/// Type of game currently running.
/// </summary>
public enum GameType
{
MapEditor = 0,
Game,
}
/// <summary>
/// Event arguments for when the RunLevel has changed in the BaseServer.
/// </summary>
public class RunLevelChangedEventArgs : EventArgs
{
/// <summary>
/// RunLevel that the BaseServer switched from.
/// </summary>
public ServerRunLevel OldLevel { get; }
/// <summary>
/// RunLevel that the BaseServers switched to.
/// </summary>
public ServerRunLevel NewLevel { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public RunLevelChangedEventArgs(ServerRunLevel oldLevel, ServerRunLevel newLevel)
{
OldLevel = oldLevel;
NewLevel = newLevel;
}
}
}