CelesteNet.Server
CelesteNetServer.cs
using Celeste.Mod.CelesteNet.DataTypes;
using Mono.Cecil;
using Mono.Options;
using MonoMod.RuntimeDetour;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Codeastysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Celeste.Mod.CelesteNet.Server {
public clast CelesteNetServer : IDisposable {
public readonly DateTime StartupTime;
public readonly long Timestamp;
public readonly CelesteNetServerSettings Settings;
public readonly DataContext Data;
public readonly TCPUDPServer TCPUDP;
public string[] ConnectionFeatures = CelesteNetUtils.ConnectionFeaturesBuiltIn;
public UserData UserData;
public readonly Channels Channels;
public bool Initialized = false;
public readonly List ModuleWrappers = new();
public readonly List Modules = new();
public readonly Dictionary ModuleMap = new();
public readonly FileSystemWatcher ModulesFSWatcher;
public readonly DetourModManager DetourModManager;
public uint PlayerCounter = 0;
public readonly RWLock ConLock = new();
public readonly HashSet Connections = new();
public readonly HashSet Sessions = new();
public readonly ConcurrentDictionary PlayersByCon = new();
public readonly ConcurrentDictionary PlayersByID = new();
private readonly ManualResetEvent ShutdownEvent = new(false);
private bool _IsAlive;
public bool IsAlive {
get => _IsAlive;
set {
if (_IsAlive == value)
return;
_IsAlive = value;
if (value)
ShutdownEvent.Reset();
else
ShutdownEvent.Set();
}
}
public CelesteNetServer()
: this(new()) {
}
public CelesteNetServer(CelesteNetServerSettings settings) {
StartupTime = DateTime.UtcNow;
Timestamp = StartupTime.Ticks / TimeSpan.TicksPerMillisecond;
Settings = settings;
DetourModManager = new();
AppDomain.CurrentDomain.astemblyResolve += (sender, args) => {
if (args.Name == null)
return null;
astemblyName name = new(args.Name);
if (ModuleWrappers.Any(wrapper => wrapper.ID == name.Name))
return null;
string path = Path.Combine(Path.GetFullPath(Settings.ModuleRoot), name.Name + ".dll");
if (File.Exists(path))
return astembly.LoadFrom(path);
return null;
};
foreach (string file in Directory.GetFiles(Path.GetFullPath(Settings.ModuleRoot)))
if (Path.GetFileName(file).StartsWith("CelesteNet.Server.") && file.EndsWith("Module.dll"))
RegisterModule(file);
Data = new();
Data.RegisterHandlersIn(this);
Channels = new(this);
UserData = new FileSystemUserData(this);
Initialized = true;
lock (Modules) {
foreach (CelesteNetServerModuleWrapper wrapper in ModuleWrappers) {
Logger.Log(LogLevel.INF, "module", $"Initializing {wrapper.ID}");
wrapper.Module?.Init(wrapper);
}
}
ModulesFSWatcher = new() {
Path = Path.GetFullPath(Settings.ModuleRoot),
Filter = "*.dll",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime
};
ModulesFSWatcher.Error += (sender, args) => {
Logger.Log(LogLevel.ERR, "module", $"Module file watcher error:\n{args.GetException()}");
};
ModulesFSWatcher.Created += OnModuleFileUpdate;
ModulesFSWatcher.Renamed += OnModuleFileUpdate;
ModulesFSWatcher.Changed += OnModuleFileUpdate;
ModulesFSWatcher.EnableRaisingEvents = true;
TCPUDP = new(this);
}
private void OnModuleFileUpdate(object sender, FileSystemEventArgs args) {
Logger.Log(LogLevel.VVV, "module", $"Module file changed: {args.FullPath}, {args.ChangeType}");
QueuedTaskHelper.Do("ReloadModuleastembly:" + args.FullPath, () => {
lock (Modules)
foreach (CelesteNetServerModuleWrapper wrapper in ModuleWrappers)
if (args.FullPath == wrapper.astemblyPath)
wrapper.Reload();
});
}
public void Start() {
if (IsAlive)
return;
Logger.Log(LogLevel.CRI, "main", "Startup");
IsAlive = true;
lock (Modules) {
foreach (CelesteNetServerModule module in Modules) {
Logger.Log(LogLevel.INF, "module", $"Starting {module.Wrapper?.ID ?? module.ToString()}");
module.Start();
}
}
Channels.Start();
TCPUDP.Start();
Logger.Log(LogLevel.CRI, "main", "Ready");
}
public void Wait() {
WaitHandle.WaitAny(new WaitHandle[] { ShutdownEvent });
ShutdownEvent.Dispose();
}
public void Dispose() {
if (!IsAlive)
return;
Logger.Log(LogLevel.CRI, "main", "Shutdown");
IsAlive = false;
ModulesFSWatcher.Dispose();
lock (Modules) {
foreach (CelesteNetServerModuleWrapper wrapper in ModuleWrappers.ToArray()) {
wrapper.Unload();
}
}
Channels.Dispose();
TCPUDP.Dispose();
ConLock.Dispose();
UserData.Dispose();
Data.Dispose();
}
public void RegisterModule(string path) {
ModuleWrappers.Add(new(this, path));
Reload:
foreach (CelesteNetServerModuleWrapper wrapper in ModuleWrappers) {
if (wrapper.Module != null ||
!wrapper.References.All(ModuleWrappers.Where(other => other.Module != null).Select(other => other.ID).Contains))
continue;
wrapper.Load();
goto Reload;
}
}
public T Get() where T : clast
=> TryGet(out T? module) ? module : throw new Exception($"Invalid module type: {typeof(T).FullName}");
public bool TryGet([NotNullWhen(true)] out T? moduleT) where T : clast {
lock (Modules) {
if (ModuleMap.TryGetValue(typeof(T), out CelesteNetServerModule? module)) {
moduleT = module as T ?? throw new Exception($"Incompatible types: Requested {typeof(T).FullName}, got {module.GetType().FullName}");
return true;
}
foreach (CelesteNetServerModule other in Modules)
if (other is T otherT) {
ModuleMap[typeof(T)] = other;
moduleT = otherT;
return true;
}
}
moduleT = null;
return false;
}
public event Action? OnConnect;
public void HandleConnect(CelesteNetConnection con) {
Logger.Log(LogLevel.INF, "main", $"New connection: {con}");
con.SendKeepAlive = true;
using (ConLock.W())
Connections.Add(con);
OnConnect?.Invoke(this, con);
con.OnDisconnect += HandleDisconnect;
}
public event Action? OnDisconnect;
public void HandleDisconnect(CelesteNetConnection con) {
Logger.Log(LogLevel.INF, "main", $"Disconnecting: {con}");
using (ConLock.W())
Connections.Remove(con);
Logger.Log(LogLevel.VVV, "main", $"Loopend send {con}");
con.Send(new DataInternalLoopend(() => {
Logger.Log(LogLevel.VVV, "main", $"Loopend run {con}");
PlayersByCon.TryGetValue(con, out CelesteNetPlayerSession? session);
session?.Dispose();
OnDisconnect?.Invoke(this, con, session);
}));
}
public event Action? OnSessionStart;
internal void InvokeOnSessionStart(CelesteNetPlayerSession session)
=> OnSessionStart?.Invoke(session);
public void Broadcast(DataType data) {
DataInternalBlob blob = new(Data, data);
using (ConLock.R())
foreach (CelesteNetConnection con in Connections) {
try {
con.Send(blob);
} catch (Exception e) {
// Whoops, it probably wasn't important anyway.
Logger.Log(LogLevel.DEV, "main", $"Broadcast (sync) failed:\n{data}\n{con}\n{e}");
}
};
}
public void BroadcastAsync(DataType data) {
DataInternalBlob blob = new(Data, data);
using (ConLock.R())
foreach (CelesteNetConnection con in Connections) {
Task.Run(() => {
try {
con.Send(blob);
} catch (Exception e) {
// Whoops, it probably wasn't important anyway.
Logger.Log(LogLevel.DEV, "main", $"Broadcast (async) failed:\n{data}\n{con}\n{e}");
}
});
}
}
#region Handlers
#endregion
}
}