Enhanced Log Output Style

This commit is contained in:
linmeideli committed 2026-07-04 14:14:21 +08:00
1 parent 6248d3c0cc
commit 0e70cafd23
32 files changed
+574 -87

No files matched your search

+2 -2
View File
@@ -15,13 +15,13 @@ public class AuthApiConfig
public AuthApiMode Mode { get; set; } = AuthApiMode.Innersloth; public AuthApiMode Mode { get; set; } = AuthApiMode.Innersloth;
public string NikoApiKey { get; set; } = ""; public string NikoApiKey { get; set; } = string.Empty;
public string NikoApiBaseUrl { get; set; } = "https://au-verify.niko233.top"; public string NikoApiBaseUrl { get; set; } = "https://au-verify.niko233.top";
public string RelayApiBaseUrl { get; set; } = "http://localhost:5100"; public string RelayApiBaseUrl { get; set; } = "http://localhost:5100";
public string RelayApiKey { get; set; } = ""; public string RelayApiKey { get; set; } = string.Empty;
public string UmeApiBaseUrl { get; set; } = "https://auverify.hayashiume.top"; public string UmeApiBaseUrl { get; set; } = "https://auverify.hayashiume.top";
+1 -1
View File
@@ -1,4 +1,4 @@
namespace Empostor.Api.Innersloth namespace Empostor.Api.Innersloth
{ {
public static class TextBox public static class TextBox
{ {
@@ -0,0 +1,24 @@
namespace Empostor.Api.Utils;
/// <summary>
/// Allows plugins to register a custom module tag for coloured
/// console output. Inject via DI and call <see cref="Register" />
/// during plugin construction.
/// </summary>
public interface IModuleTagRegistry
{
/// <summary>
/// Register a namespace → tag mapping.
/// </summary>
/// <param name="namespacePrefix">
/// Full or partial namespace prefix, e.g. "Empostor.Plugin.MyMod".
/// </param>
/// <param name="tag">
/// Short tag shown in console brackets, e.g. "MyMod".
/// </param>
/// <param name="ansiColor">
/// Optional ANSI SGR colour, e.g. "\x1b[92m" for bright green.
/// Omit for auto-assignment from the palette.
/// </param>
void Register(string namespacePrefix, string tag, string? ansiColor = null);
}
+4 -2
View File
@@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Empostor.Api.Plugins; using Empostor.Api.Plugins;
using Empostor.Api.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Empostor.Plugin.Chat; namespace Empostor.Plugin.Chat;
@@ -9,14 +10,15 @@ public sealed class ChatPlugin : PluginBase
{ {
private readonly ILogger<ChatPlugin> _logger; private readonly ILogger<ChatPlugin> _logger;
public ChatPlugin(ILogger<ChatPlugin> logger) public ChatPlugin(ILogger<ChatPlugin> logger, IModuleTagRegistry tags)
{ {
_logger = logger; _logger = logger;
tags.Register("Empostor.Plugin.Chat", "Chat", "\x1b[92m");
} }
public override ValueTask EnableAsync() public override ValueTask EnableAsync()
{ {
_logger.LogInformation("[Chat] Enabled — limiting message length and logging chat."); _logger.LogInformation("Chat plugin enabled — logging chat and limiting message length.");
return default; return default;
} }
+4 -12
View File
@@ -1,7 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using Empostor.Api;
using Empostor.Api.Events.Player; using Empostor.Api.Events.Player;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -22,33 +21,26 @@ public sealed class ChatService
public void HandleChatMessage(IPlayerChatEvent e) public void HandleChatMessage(IPlayerChatEvent e)
{ {
var client = e.ClientPlayer.Client; var playerName = e.ClientPlayer.Client.Name;
var clientId = client.Id;
var playerName = client.Name;
var slot = e.ClientPlayer.Character?.PlayerId ?? 0; var slot = e.ClientPlayer.Character?.PlayerId ?? 0;
var gameCode = Api.Innersloth.GameCodeParser.IntToGameName(e.Game.Code);
int channel;
string channelName; string channelName;
if (e.IsCancelled) if (e.IsCancelled)
{ {
channel = 0;
channelName = "Canceled"; channelName = "Canceled";
} }
else if (!e.SendToAllPlayers) else if (!e.SendToAllPlayers)
{ {
channel = 255;
channelName = "Command"; channelName = "Command";
} }
else else
{ {
channel = -1;
channelName = "Public"; channelName = "Public";
} }
_logger.LogInformation( _logger.LogInformation(
"{Code} - [{ClientId}][{Name}][{Slot}] => [{Channel}]{ChannelName}: [{Message}]", "✉ {Name} [{Slot}] → {ChannelName}: {Message}",
gameCode, clientId, playerName, slot, channel, channelName, e.Message); playerName, slot, channelName, e.Message);
var isHost = e.ClientPlayer.IsHost; var isHost = e.ClientPlayer.IsHost;
var maxLength = isHost ? _config.HostMaxMessageLength : _config.PlayerMaxMessageLength; var maxLength = isHost ? _config.HostMaxMessageLength : _config.PlayerMaxMessageLength;
@@ -56,7 +48,7 @@ public sealed class ChatService
if (e.Message.Length > maxLength) if (e.Message.Length > maxLength)
{ {
_logger.LogWarning( _logger.LogWarning(
"Cancelling chat message from {PlayerName} ({PlayerType}) of {Length} chars (max: {MaxLength}): too long", "✉ {PlayerName} | {PlayerType} | blocked: {Length}/{MaxLength} chars",
playerName, isHost ? "host" : "player", e.Message.Length, maxLength); playerName, isHost ? "host" : "player", e.Message.Length, maxLength);
e.PlayerControl.SendChatToPlayerAsync(_config.TooLongMessage, e.PlayerControl); e.PlayerControl.SendChatToPlayerAsync(_config.TooLongMessage, e.PlayerControl);
@@ -12,7 +12,7 @@ public sealed class FixedCodePlugin : PluginBase
public FixedCodePlugin(ILogger<FixedCodePlugin> logger, FixedCodeListener listener) public FixedCodePlugin(ILogger<FixedCodePlugin> logger, FixedCodeListener listener)
{ {
_logger = logger; _logger = logger;
_listener = listener; _listener = listener;
} }
@@ -15,7 +15,7 @@ namespace Empostor.Plugins.FriendCodeValidator
private static readonly Regex Pattern = private static readonly Regex Pattern =
new(@"^([a-zA-Z]+)#(\d{4})$", RegexOptions.Compiled); new(@"^([a-zA-Z]+)#(\d{4})$", RegexOptions.Compiled);
private static readonly HashSet<string> WordsA = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> WordsA = new(StringComparer.OrdinalIgnoreCase)
{ {
"abiding", "abiding",
"abiotic", "abiotic",
@@ -2420,7 +2420,7 @@ namespace Empostor.Plugins.FriendCodeValidator
"zonal", "zonal",
}; };
private static readonly HashSet<string> WordsB = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> WordsB = new(StringComparer.OrdinalIgnoreCase)
{ {
"ability", "ability",
"absence", "absence",
@@ -5402,7 +5402,7 @@ namespace Empostor.Plugins.FriendCodeValidator
// Try every split point: left+right must be (A,B) or (B,A) // Try every split point: left+right must be (A,B) or (B,A)
for (var i = 1; i < name.Length; i++) for (var i = 1; i < name.Length; i++)
{ {
var left = name[..i]; var left = name[..i];
var right = name[i..]; var right = name[i..];
if ((WordsA.Contains(left) && WordsB.Contains(right)) || if ((WordsA.Contains(left) && WordsB.Contains(right)) ||
(WordsB.Contains(left) && WordsA.Contains(right))) (WordsB.Contains(left) && WordsA.Contains(right)))
@@ -14,7 +14,7 @@ public sealed class TitleEventListener : IEventListener
public TitleEventListener(ILogger<TitleEventListener> logger, TitleStore store) public TitleEventListener(ILogger<TitleEventListener> logger, TitleStore store)
{ {
_logger = logger; _logger = logger;
_store = store; _store = store;
} }
[EventListener] [EventListener]
@@ -51,7 +51,7 @@ public sealed class WelcomeMessageService
if (colon < 1) continue; if (colon < 1) continue;
var lang = line[..colon].Trim(); var lang = line[..colon].Trim();
var msg = line[(colon + 1)..].Trim(); var msg = line[(colon + 1)..].Trim();
if (!string.IsNullOrEmpty(msg)) if (!string.IsNullOrEmpty(msg))
_templates[lang] = msg; _templates[lang] = msg;
} }
@@ -71,7 +71,7 @@ public sealed class WelcomeMessageService
return template return template
.Replace("{Name}", name, StringComparison.OrdinalIgnoreCase) .Replace("{Name}", name, StringComparison.OrdinalIgnoreCase)
.Replace("{FriendCode}", friendCode ?? "None", StringComparison.OrdinalIgnoreCase) .Replace("{FriendCode}", friendCode ?? "None", StringComparison.OrdinalIgnoreCase)
.Replace("{GameCode}", gameCode, StringComparison.OrdinalIgnoreCase); .Replace("{GameCode}", gameCode, StringComparison.OrdinalIgnoreCase);
} }
private const string DefaultContent = """ private const string DefaultContent = """
@@ -45,7 +45,9 @@ public sealed class CommandService
public async ValueTask<bool> TryHandleAsync(CommandContext ctx) public async ValueTask<bool> TryHandleAsync(CommandContext ctx)
{ {
if (!_commands.TryGetValue(ctx.Name, out var command)) if (!_commands.TryGetValue(ctx.Name, out var command))
{
return false; return false;
}
try try
{ {
@@ -32,7 +32,9 @@ public sealed class MaxCommand : ICommand
var msg = ctx.GetString("command.max.set").Format(count).Get(); var msg = ctx.GetString("command.max.set").Format(count).Get();
if (count > 15) if (count > 15)
{
msg += "\n" + ctx.GetString("command.max.warning"); msg += "\n" + ctx.GetString("command.max.warning");
}
await ctx.PlayerControl.SendChatToPlayerAsync(msg, ctx.PlayerControl); await ctx.PlayerControl.SendChatToPlayerAsync(msg, ctx.PlayerControl);
return true; return true;
+1 -1
View File
@@ -827,7 +827,7 @@ namespace Empostor.Server.Http
friendCode = c.FriendCode ?? "—", friendCode = c.FriendCode ?? "—",
gameVersion = c.GameVersion.ToString(), gameVersion = c.GameVersion.ToString(),
platform = c.PlatformSpecificData?.Platform.ToString() ?? "Unknown", platform = c.PlatformSpecificData?.Platform.ToString() ?? "Unknown",
platformName = c.PlatformSpecificData?.PlatformName ?? "", platformName = c.PlatformSpecificData?.PlatformName ?? string.Empty,
language = LanguageHelper.GetDisplayName(c.Language), language = LanguageHelper.GetDisplayName(c.Language),
level = level is null or 0 or uint.MaxValue ? (int?)null : (int)level.Value, level = level is null or 0 or uint.MaxValue ? (int?)null : (int)level.Value,
inGame = c.Player != null, inGame = c.Player != null,
@@ -18,9 +18,14 @@ internal static class AdminTemplateDefaults
var loginPath = Path.Combine(PagesDir, "login.html"); var loginPath = Path.Combine(PagesDir, "login.html");
var adminPath = Path.Combine(PagesDir, "admin.html"); var adminPath = Path.Combine(PagesDir, "admin.html");
if (!File.Exists(loginPath)) if (!File.Exists(loginPath))
{
File.WriteAllText(loginPath, LoginHtml); File.WriteAllText(loginPath, LoginHtml);
}
if (!File.Exists(adminPath)) if (!File.Exists(adminPath))
{
File.WriteAllText(adminPath, AdminHtml); File.WriteAllText(adminPath, AdminHtml);
}
} }
catch catch
{ {
+10 -10
View File
@@ -85,25 +85,25 @@ public sealed class MonitorController : ControllerBase
public sealed class MonitorStatus public sealed class MonitorStatus
{ {
[JsonPropertyName("status")] [JsonPropertyName("status")]
public string Status { get; set; } = ""; public string Status { get; set; } = string.Empty;
[JsonPropertyName("timestamp")] [JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = ""; public string Timestamp { get; set; } = string.Empty;
[JsonPropertyName("uptime_seconds")] [JsonPropertyName("uptime_seconds")]
public long UptimeSeconds { get; set; } public long UptimeSeconds { get; set; }
[JsonPropertyName("uptime_display")] [JsonPropertyName("uptime_display")]
public string UptimeDisplay { get; set; } = ""; public string UptimeDisplay { get; set; } = string.Empty;
[JsonPropertyName("version")] [JsonPropertyName("version")]
public string Version { get; set; } = ""; public string Version { get; set; } = string.Empty;
[JsonPropertyName("runtime")] [JsonPropertyName("runtime")]
public string Runtime { get; set; } = ""; public string Runtime { get; set; } = string.Empty;
[JsonPropertyName("platform")] [JsonPropertyName("platform")]
public string Platform { get; set; } = ""; public string Platform { get; set; } = string.Empty;
[JsonPropertyName("processors")] [JsonPropertyName("processors")]
public int Processors { get; set; } public int Processors { get; set; }
@@ -127,17 +127,17 @@ public sealed class MonitorStatus
public sealed class MonitorGame public sealed class MonitorGame
{ {
[JsonPropertyName("code")] [JsonPropertyName("code")]
public string Code { get; set; } = ""; public string Code { get; set; } = string.Empty;
[JsonPropertyName("state")] [JsonPropertyName("state")]
public string State { get; set; } = ""; public string State { get; set; } = string.Empty;
[JsonPropertyName("map")] [JsonPropertyName("map")]
public string Map { get; set; } = ""; public string Map { get; set; } = string.Empty;
[JsonPropertyName("player_count")] [JsonPropertyName("player_count")]
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
[JsonPropertyName("host")] [JsonPropertyName("host")]
public string Host { get; set; } = ""; public string Host { get; set; } = string.Empty;
} }
+17 -4
View File
@@ -27,7 +27,9 @@ public sealed class PrivacyController : ControllerBase
public IActionResult GetPrivacy() public IActionResult GetPrivacy()
{ {
if (!System.IO.File.Exists(PrivacyFile)) if (!System.IO.File.Exists(PrivacyFile))
{
return Content(DefaultPrivacyHtml(), "text/html; charset=utf-8"); return Content(DefaultPrivacyHtml(), "text/html; charset=utf-8");
}
var html = System.IO.File.ReadAllText(PrivacyFile); var html = System.IO.File.ReadAllText(PrivacyFile);
return Content(html, "text/html; charset=utf-8"); return Content(html, "text/html; charset=utf-8");
@@ -43,15 +45,17 @@ public sealed class PrivacyController : ControllerBase
try try
{ {
var doc = System.Text.Json.JsonDocument.Parse(body); var doc = System.Text.Json.JsonDocument.Parse(body);
var content = doc.RootElement.GetProperty("content").GetString() ?? ""; var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty;
var token = doc.RootElement.GetProperty("token").GetString() ?? ""; var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty;
var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN") var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN")
?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN") ?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN")
?? "empostor"; ?? "empostor";
if (token != adminToken) if (token != adminToken)
{
return Unauthorized(new { error = "Invalid token." }); return Unauthorized(new { error = "Invalid token." });
}
System.IO.File.WriteAllText(PrivacyFile, content); System.IO.File.WriteAllText(PrivacyFile, content);
_logger.LogInformation("PrivacyControllerPrivacy policy updated."); _logger.LogInformation("PrivacyControllerPrivacy policy updated.");
@@ -65,15 +69,24 @@ public sealed class PrivacyController : ControllerBase
private void EnsureFile() private void EnsureFile()
{ {
if (_initialized) return; if (_initialized)
{
return;
}
lock (_initLock) lock (_initLock)
{ {
if (_initialized) return; if (_initialized)
{
return;
}
_initialized = true; _initialized = true;
if (!Directory.Exists(PagesDir)) if (!Directory.Exists(PagesDir))
{
Directory.CreateDirectory(PagesDir); Directory.CreateDirectory(PagesDir);
}
if (!System.IO.File.Exists(PrivacyFile)) if (!System.IO.File.Exists(PrivacyFile))
{ {
+2 -2
View File
@@ -391,7 +391,7 @@ public sealed class TokenController : ControllerBase
var putBody = JsonSerializer.SerializeToUtf8Bytes(new NikoPutRequest var putBody = JsonSerializer.SerializeToUtf8Bytes(new NikoPutRequest
{ {
ApiKey = _authApiConfig.NikoApiKey, ApiKey = _authApiConfig.NikoApiKey,
FriendCode = "", FriendCode = string.Empty,
}); });
var putReq = new HttpRequestMessage(HttpMethod.Put, apiUrl) var putReq = new HttpRequestMessage(HttpMethod.Put, apiUrl)
@@ -736,7 +736,7 @@ public sealed class TokenController : ControllerBase
public required string ApiKey { get; init; } public required string ApiKey { get; init; }
[JsonPropertyName("FriendCode")] [JsonPropertyName("FriendCode")]
public string FriendCode { get; init; } = ""; public string FriendCode { get; init; } = string.Empty;
} }
private sealed class NikoCreateResponse private sealed class NikoCreateResponse
+2 -1
View File
@@ -50,7 +50,8 @@ namespace Empostor.Server.Net
if (Player != null && Player.IsHost) if (Player != null && Player.IsHost)
{ {
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch { var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch
{
CheatingHostMode.Always => true, CheatingHostMode.Always => true,
CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag, CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag,
CheatingHostMode.Never => false, CheatingHostMode.Never => false,
@@ -1008,8 +1008,8 @@ namespace Empostor.Server.Net.Inner.Objects
{ {
if (PlayerInfo.PlayerLevel != level) if (PlayerInfo.PlayerLevel != level)
{ {
_logger.LogInformation("{Code} - Player {Name} ({Id}) level changed {Old} -> {New}", _logger.LogInformation("★ {Name} ({Id}) | Level {Old} → {New}",
_game.Code, sender.Client.Name, sender.Client.Id, PlayerInfo.PlayerLevel, level); sender.Client.Name, sender.Client.Id, PlayerInfo.PlayerLevel, level);
} }
PlayerInfo.PlayerLevel = level; PlayerInfo.PlayerLevel = level;
@@ -1168,7 +1168,9 @@ namespace Empostor.Server.Net.Inner.Objects
}; };
var handled = await _commandService.TryHandleAsync(ctx); var handled = await _commandService.TryHandleAsync(ctx);
if (handled) if (handled)
{
return false; return false;
}
} }
var @event = new PlayerChatEvent(Game, sender, this, message); var @event = new PlayerChatEvent(Game, sender, this, message);
@@ -109,7 +109,7 @@ namespace Empostor.Server.Net.Manager
if (versionCompare == ICompatibilityManager.VersionCompareResult.ServerTooOld if (versionCompare == ICompatibilityManager.VersionCompareResult.ServerTooOld
&& _compatibilityConfig.AllowFutureGameVersions && platformSpecificData != null) && _compatibilityConfig.AllowFutureGameVersions && platformSpecificData != null)
{ {
_logger.LogWarning("Client [{Id}]{Name} connected using future version {Version}", id, name, clientVersion); _logger.LogWarning("#{Id} {Name} | using future version {Version}", id, name, clientVersion);
} }
else if (versionCompare != ICompatibilityManager.VersionCompareResult.Compatible else if (versionCompare != ICompatibilityManager.VersionCompareResult.Compatible
|| platformSpecificData == null) || platformSpecificData == null)
@@ -134,7 +134,7 @@ namespace Empostor.Server.Net.Manager
return; return;
} }
_logger.LogInformation("Client [{Id}]{Name} connected with host authority", id, name); _logger.LogInformation("#{Id} {Name} | host authority enabled", id, name);
} }
if (name.Length > 10) { await connection.CustomDisconnectAsync(DisconnectReason.Custom, DisconnectMessages.UsernameLength); return; } if (name.Length > 10) { await connection.CustomDisconnectAsync(DisconnectReason.Custom, DisconnectMessages.UsernameLength); return; }
@@ -148,7 +148,7 @@ namespace Empostor.Server.Net.Manager
var platformStr = platformSpecificData?.Platform.ToString() ?? "Unknown"; var platformStr = platformSpecificData?.Platform.ToString() ?? "Unknown";
var reactorMods = connection.GetReactorMods(); var reactorMods = connection.GetReactorMods();
var reactorStr = reactorMods?.Mods is { Count: > 0 } mods var reactorStr = reactorMods?.Mods is { Count: > 0 } mods
? $" [Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}]" ? $" | Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}"
: string.Empty; : string.Empty;
// Primary: match by delta port (nonce) // Primary: match by delta port (nonce)
@@ -184,13 +184,13 @@ namespace Empostor.Server.Net.Manager
} }
_logger.LogInformation( _logger.LogInformation(
"Client [{Id}]{Name} from port {Port} {Location}[{Lang}] [{Platform}] is authorized as {FriendCode} {HashPuid}{Reactor}", "#{Id} {Name} | port {Port} | {Location} | {Lang} | {Platform} | FC {FriendCode} | {HashPuid}{Reactor}",
id, name, deltaPort, location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr); id, name, deltaPort, location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr);
} }
else else
{ {
_logger.LogWarning( _logger.LogWarning(
"Client [{Id}]{Name} port {Port} has no auth info, falling back to IP", "#{Id} {Name} | port {Port} has no auth info, falling back to IP",
id, name, deltaPort); id, name, deltaPort);
} }
} }
@@ -203,13 +203,13 @@ namespace Empostor.Server.Net.Manager
{ {
friendCode = authInfo.FriendCode; friendCode = authInfo.FriendCode;
_logger.LogInformation( _logger.LogInformation(
"Client [{Id}]{Name} from {Ip} {Location}[{Lang}] [{Platform}] is authorized as {FriendCode} {HashPuid}{Reactor}", "#{Id} {Name} | {Ip} | {Location} | {Lang} | {Platform} | FC {FriendCode} | {HashPuid}{Reactor}",
id, name, NormalizeIp(clientIp), location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr); id, name, NormalizeIp(clientIp), location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr);
} }
else else
{ {
_logger.LogWarning( _logger.LogWarning(
"Client [{Id}]{Name}: no auth info found (port={Port}, ip={Ip} {Location}) [{Lang}] [{Platform}]{Reactor}", "#{Id} {Name} | no auth | port {Port} | {Ip} | {Location} | {Lang} | {Platform}{Reactor}",
id, name, deltaPort, NormalizeIp(clientIp), location, lang, platformStr, reactorStr); id, name, deltaPort, NormalizeIp(clientIp), location, lang, platformStr, reactorStr);
} }
} }
@@ -220,7 +220,7 @@ namespace Empostor.Server.Net.Manager
client.DeltaPort = deltaPort; client.DeltaPort = deltaPort;
client.Id = id; client.Id = id;
_logger.LogDebug("Client [{Id}]{Name} connected with FriendCode={FC} DeltaPort={DP}", id, name, client.FriendCode ?? "(none)", deltaPort); _logger.LogDebug("#{Id} {Name} | connected | FC={FC} | port {DP}", id, name, client.FriendCode ?? "(none)", deltaPort);
_clients.TryAdd(id, client); _clients.TryAdd(id, client);
await _eventManager.CallAsync(new ClientConnectedEvent(connection, client)); await _eventManager.CallAsync(new ClientConnectedEvent(connection, client));
} }
+1 -1
View File
@@ -206,7 +206,7 @@ namespace Empostor.Server.Net.State
continue; continue;
} }
_logger.LogWarning("Couldn't find spawnable object {0}.", objectId); _logger.LogWarning("Couldn't find spawnable object {ObjectId}", objectId);
break; break;
} }
+7 -12
View File
@@ -72,7 +72,7 @@ namespace Empostor.Server.Net.State
public async ValueTask HandleKickPlayer(int playerId, bool isBan) public async ValueTask HandleKickPlayer(int playerId, bool isBan)
{ {
_logger.LogInformation("{Code} - Kicking player {PlayerId} (Ban={IsBan})", Code, playerId, isBan); _logger.LogInformation("✂ Player {PlayerId} | Ban={IsBan}", playerId, isBan);
using (var kickMsg = MessageWriter.Get(MessageType.Reliable)) using (var kickMsg = MessageWriter.Get(MessageType.Reliable))
{ {
WriteKickPlayerMessage(kickMsg, false, playerId, isBan); WriteKickPlayerMessage(kickMsg, false, playerId, isBan);
@@ -121,25 +121,20 @@ namespace Empostor.Server.Net.State
private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew) private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew)
{ {
var client = sender.Client; var client = sender.Client;
var clientIp = client.Connection.EndPoint?.Address;
var ipStr = clientIp?.IsIPv4MappedToIPv6 == true ? clientIp.MapToIPv4().ToString() : clientIp?.ToString() ?? "?";
var remotePort = (client.Connection.EndPoint as System.Net.IPEndPoint)?.Port ?? 0;
var authority = client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false"; var authority = client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false";
var version = client.GameVersion.ToString(); var version = client.GameVersion.ToString();
var hashPuid = client.ProductUserId?.Length >= 9 ? client.ProductUserId[..9] : client.ProductUserId ?? "0";
var deltaPort = client.DeltaPort; var deltaPort = client.DeltaPort;
var location = await _ipGeo.GetLocationAsync(clientIp);
var lang = LanguageHelper.GetDisplayName(client.Language); var lang = LanguageHelper.GetDisplayName(client.Language);
var platform = client.PlatformSpecificData?.Platform.ToString() ?? "Unknown"; var platform = client.PlatformSpecificData?.Platform.ToString() ?? "Unknown";
var platformName = client.PlatformSpecificData?.PlatformName; var platformName = client.PlatformSpecificData?.PlatformName;
var platformStr = string.IsNullOrEmpty(platformName) ? platform : $"{platform}/{platformName}"; var platformStr = string.IsNullOrEmpty(platformName) ? platform : $"{platform}/{platformName}";
var reactorMods = client.GetReactorMods(); var reactorMods = client.GetReactorMods();
var reactorStr = reactorMods?.Mods is { Count: > 0 } mods var reactorStr = reactorMods?.Mods is { Count: > 0 } mods
? $" [Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}]" ? $" | Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}"
: string.Empty; : string.Empty;
_logger.LogInformation("{Code} - Player {Name} ({HashPuid}) ({Id}) [{Lang}] [{Platform}] is joining from ({Ip} {Location}:{RemotePort}) with v{Version}, Authority:{Authority}, port: {DeltaPort}{Reactor}", _logger.LogInformation("▶ {Name} ({Id}) joined | {Lang} | {Platform} | v{Version} | Authority:{Authority} | port {DeltaPort}{Reactor}",
Code, sender.Client.Name, hashPuid, sender.Client.Id, lang, platformStr, ipStr, location, remotePort, version, authority, deltaPort, reactorStr); sender.Client.Name, sender.Client.Id, lang, platformStr, version, authority, deltaPort, reactorStr);
if (isNew) if (isNew)
{ {
@@ -229,8 +224,8 @@ namespace Empostor.Server.Net.State
private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew) private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew)
{ {
var authority = sender.Client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false"; var authority = sender.Client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false";
_logger.LogInformation("{Code} - Player {Name} ({Id}) is rejoining. Player Authority : {Authority}, port: {DeltaPort}", _logger.LogInformation("↻ {Name} ({Id}) rejoined | Authority:{Authority} | port {DeltaPort}",
Code, sender.Client.Name, sender.Client.Id, authority, sender.Client.DeltaPort); sender.Client.Name, sender.Client.Id, authority, sender.Client.DeltaPort);
if (isNew) if (isNew)
{ {
@@ -239,7 +234,7 @@ namespace Empostor.Server.Net.State
if (sender.Client.Id == HostId) if (sender.Client.Id == HostId)
{ {
_logger.LogInformation("{Code} - Host {Name} ({Id}) is rejoining.", Code, sender.Client.Name, sender.Client.Id); _logger.LogInformation("↻ {Name} ({Id}) host rejoined", sender.Client.Name, sender.Client.Id);
GameState = GameStates.NotStarted; GameState = GameStates.NotStarted;
await HandleJoinGameNew(sender, false); await HandleJoinGameNew(sender, false);
await CheckLimboPlayers(); await CheckLimboPlayers();
+4 -4
View File
@@ -33,8 +33,8 @@ namespace Empostor.Server.Net.State
return false; return false;
} }
_logger.LogInformation("{Code} - Player {Name} ({Id}) has left for \"{Reason}\" hashpuid : {HashPuid}", _logger.LogInformation("◀ {Name} ({Id}) left | {Reason} | {HashPuid}",
Code, player.Client.Name, playerId, reason, player.Client.Name, playerId, reason,
player.Client.ProductUserId?.Length >= 9 ? player.Client.ProductUserId[..9] : player.Client.ProductUserId ?? "0"); player.Client.ProductUserId?.Length >= 9 ? player.Client.ProductUserId[..9] : player.Client.ProductUserId ?? "0");
if (GameState == GameStates.Starting || GameState == GameStates.Started || GameState == GameStates.NotStarted) if (GameState == GameStates.Starting || GameState == GameStates.Started || GameState == GameStates.NotStarted)
{ {
@@ -71,7 +71,7 @@ namespace Empostor.Server.Net.State
await Task.Delay(_timeoutConfig.ConnectionTimeout); await Task.Delay(_timeoutConfig.ConnectionTimeout);
if (player.Client.Connection.IsConnected && player.Client.Connection is HazelConnection hazel) if (player.Client.Connection.IsConnected && player.Client.Connection is HazelConnection hazel)
{ {
_logger.LogInformation("{Code} - Player {Name} ({Id}) kept connection open after leaving, disposing.", Code, player.Client.Name, playerId); _logger.LogInformation("◀ {Name} ({Id}) left | kept connection, disposing", player.Client.Name, playerId);
await player.Client.DisconnectAsync(isBan ? DisconnectReason.Banned : DisconnectReason.Kicked); await player.Client.DisconnectAsync(isBan ? DisconnectReason.Banned : DisconnectReason.Kicked);
} }
}); });
@@ -103,7 +103,7 @@ namespace Empostor.Server.Net.State
} }
HostId = host.Client.Id; HostId = host.Client.Id;
_logger.LogInformation("{Code} - Assigned {Name} ({Id}) as new host.", Code, host.Client.Name, host.Client.Id); _logger.LogInformation("★ {Name} ({Id}) became host", host.Client.Name, host.Client.Id);
if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost) if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost)
{ {
GameState = GameStates.NotStarted; GameState = GameStates.NotStarted;
+1 -5
View File
@@ -16,7 +16,6 @@ using Empostor.Api.Net.Manager;
using Empostor.Api.Net.Messages.S2C; using Empostor.Api.Net.Messages.S2C;
using Empostor.Server.Events; using Empostor.Server.Events;
using Empostor.Server.Net.Manager; using Empostor.Server.Net.Manager;
using Empostor.Server.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -34,7 +33,6 @@ namespace Empostor.Server.Net.State
private readonly ICompatibilityManager _compatibilityManager; private readonly ICompatibilityManager _compatibilityManager;
private readonly CompatibilityConfig _compatibilityConfig; private readonly CompatibilityConfig _compatibilityConfig;
private readonly TimeoutConfig _timeoutConfig; private readonly TimeoutConfig _timeoutConfig;
private readonly IpGeolocationService _ipGeo;
public Game( public Game(
ILogger<Game> logger, ILogger<Game> logger,
@@ -48,8 +46,7 @@ namespace Empostor.Server.Net.State
IEventManager eventManager, IEventManager eventManager,
ICompatibilityManager compatibilityManager, ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig, IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig, IOptions<TimeoutConfig> timeoutConfig)
IpGeolocationService ipGeo)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
@@ -68,7 +65,6 @@ namespace Empostor.Server.Net.State
_compatibilityManager = compatibilityManager; _compatibilityManager = compatibilityManager;
_compatibilityConfig = compatibilityConfig.Value; _compatibilityConfig = compatibilityConfig.Value;
_timeoutConfig = timeoutConfig.Value; _timeoutConfig = timeoutConfig.Value;
_ipGeo = ipGeo;
Items = new ConcurrentDictionary<object, object>(); Items = new ConcurrentDictionary<object, object>();
} }
+7 -4
View File
@@ -54,13 +54,15 @@ namespace Empostor.Server
private static int Main(string[] args) private static int Main(string[] args)
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Source} - {Message:lj}{NewLine}{Exception}") .WriteTo.Console(formatter: new EmpostorConsoleFormatter())
.Enrich.With<ShortSourceEnricher>() .Enrich.With<ShortSourceEnricher>()
.CreateBootstrapLogger(); .CreateBootstrapLogger();
try try
{ {
Log.Information("Empostor v{Version} starting", DotnetUtils.Version); Log.Information("Empostor v{Version} starting", DotnetUtils.Version);
CreateHostBuilder(args).Build().Run(); var host = CreateHostBuilder(args).Build();
StartupBanner.Print(host.Services.GetRequiredService<IConfiguration>());
host.Run();
return 0; return 0;
} }
catch (Exception ex) catch (Exception ex)
@@ -163,6 +165,7 @@ namespace Empostor.Server
}); });
services.AddSingleton<IpGeolocationService>(); services.AddSingleton<IpGeolocationService>();
services.AddSingleton<IModuleTagRegistry>(ModuleTagRegistry.Instance);
services.AddSingleton<ICompatibilityManager, CompatibilityManager>(); services.AddSingleton<ICompatibilityManager, CompatibilityManager>();
services.AddSingleton<ClientManager>(); services.AddSingleton<ClientManager>();
services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>()); services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>());
@@ -255,12 +258,12 @@ namespace Empostor.Server
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.Enrich.With<ShortSourceEnricher>() .Enrich.With<ShortSourceEnricher>()
.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Source} - {Message:lj}{NewLine}{Exception}") .WriteTo.Console(formatter: new EmpostorConsoleFormatter())
.WriteTo.File( .WriteTo.File(
"Log/empostor-.log", "Log/empostor-.log",
rollingInterval: RollingInterval.Day, rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 31, retainedFileCountLimit: 31,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Source} - {Message:lj}{NewLine}{Exception}") outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{Module}] {Message}{NewLine}{Exception}")
.ReadFrom.Configuration(context.Configuration, .ReadFrom.Configuration(context.Configuration,
new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles)); new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles));
AssemblyLoadContext.Default.Resolving -= TryLoad; AssemblyLoadContext.Default.Resolving -= TryLoad;
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.IO;
using Serilog.Events;
using Serilog.Formatting;
namespace Empostor.Server.Utils;
public sealed class EmpostorConsoleFormatter : ITextFormatter
{
private readonly bool _useColor;
private const string Reset = "\x1b[0m";
private const string Bold = "\x1b[1m";
private const string Dim = "\x1b[2m";
private const string FgRed = "\x1b[91m";
private const string FgGreen = "\x1b[92m";
private const string FgYellow = "\x1b[93m";
private const string FgBlue = "\x1b[94m";
private const string FgMagenta = "\x1b[95m";
private const string FgCyan = "\x1b[96m";
private const string FgWhite = "\x1b[97m";
private const string FgGray = "\x1b[90m";
private const string FgDark = "\x1b[37m";
private const string BgRed = "\x1b[41m";
private static readonly Dictionary<string, string> ModuleColors = new()
{
["Auth"] = FgMagenta,
["Game"] = FgCyan,
["Chat"] = FgGreen,
["Reactor"] = "\x1b[35m",
["Admin"] = FgBlue,
["Cmd"] = FgYellow,
["Ban"] = FgRed,
["Plugin"] = "\x1b[36m",
["Discord"] = "\x1b[34m",
["Stat"] = FgCyan,
};
public EmpostorConsoleFormatter()
{
_useColor = !Console.IsOutputRedirected;
}
public void Format(LogEvent logEvent, TextWriter output)
{
if (_useColor)
{
FormatColored(logEvent, output);
}
else
{
FormatPlain(logEvent, output);
}
}
private void FormatColored(LogEvent logEvent, TextWriter output)
{
var ts = logEvent.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz");
output.Write(Dim);
output.Write(FgGray);
output.Write(ts);
output.Write(Reset);
output.Write(' ');
var (levelFg, levelBg, levelText) = LevelStyle(logEvent.Level);
if (levelBg.Length > 0)
{
output.Write(levelBg);
}
output.Write(levelFg);
output.Write(Bold);
output.Write(levelText);
output.Write(Reset);
output.Write(' ');
var module = GetModule(logEvent);
var modColor = GetModuleColor(module);
output.Write(modColor);
output.Write('[');
output.Write(module);
output.Write(']');
output.Write(Reset);
output.Write(' ');
output.Write(logEvent.RenderMessage());
output.WriteLine();
if (logEvent.Exception != null)
{
output.Write(FgRed);
output.Write(logEvent.Exception);
output.Write(Reset);
output.WriteLine();
}
}
private static void FormatPlain(LogEvent logEvent, TextWriter output)
{
var ts = logEvent.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz");
var level = FormatLevel(logEvent.Level);
var module = GetModule(logEvent);
output.Write($"{ts} [{level}] [{module}] ");
output.Write(logEvent.RenderMessage());
output.WriteLine();
if (logEvent.Exception != null)
{
output.Write(logEvent.Exception.ToString());
output.WriteLine();
}
}
private static string GetModule(LogEvent logEvent)
{
if (logEvent.Properties.TryGetValue("Module", out var v) &&
v is ScalarValue sv && sv.Value is string module)
{
return module;
}
return "Empostor";
}
private static string GetModuleColor(string module)
{
if (ModuleColors.TryGetValue(module, out var c))
return c;
// Check plugin-registered tags
var regColor = ModuleTagRegistry.Instance.GetColor(module);
return regColor ?? FgWhite;
}
private static string FormatLevel(LogEventLevel level) => level switch
{
LogEventLevel.Verbose => "VRB",
LogEventLevel.Debug => "DBG",
LogEventLevel.Information => "INF",
LogEventLevel.Warning => "WRN",
LogEventLevel.Error => "ERR",
LogEventLevel.Fatal => "FTL",
_ => "INF",
};
private static (string fg, string bg, string text) LevelStyle(LogEventLevel level) => level switch
{
LogEventLevel.Fatal => (FgWhite, BgRed, "FTL"),
LogEventLevel.Error => (FgRed, string.Empty, "ERR"),
LogEventLevel.Warning => (FgYellow, string.Empty, "WRN"),
LogEventLevel.Information => (FgGray, string.Empty, "INF"),
LogEventLevel.Debug => (FgDark, string.Empty, "DBG"),
LogEventLevel.Verbose => (Dim + FgDark, string.Empty, "VRB"),
_ => (FgGray, string.Empty, "INF"),
};
}
@@ -0,0 +1,79 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Empostor.Api.Utils;
namespace Empostor.Server.Utils;
/// <summary>
/// Thread-safe registry for plugin module tags.
/// Plugins inject <see cref="IModuleTagRegistry" /> and call <see cref="Register" /> to assign a short tag (and optional ANSI colour) to their namespace prefix.
/// The enricher and console formatter consult this registry at runtime.
/// </summary>
public sealed class ModuleTagRegistry : IModuleTagRegistry
{
public static ModuleTagRegistry Instance { get; } = new();
private readonly List<(string Prefix, string Tag, string? Color)> _entries = new();
private readonly ConcurrentDictionary<string, string> _colorCache = new();
private int _autoIdx = -1;
private static readonly string[] AutoPalette =
{
"\x1b[96m", // Bright Cyan
"\x1b[95m", // Bright Magenta
"\x1b[94m", // Bright Blue
"\x1b[92m", // Bright Green
"\x1b[93m", // Bright Yellow
"\x1b[91m", // Bright Red
};
private ModuleTagRegistry()
{
}
/// <summary>
/// Register a namespace → tag mapping.
/// </summary>
/// <param name="namespacePrefix">Full or partial namespace prefix, e.g. "Empostor.Plugin.MyMod".</param>
/// <param name="tag">Short tag shown in brackets, e.g. "MyMod" (keep it short).</param>
/// <param name="ansiColor">
/// Optional ANSI SGR colour code (e.g. "\x1b[92m" for bright green).
/// When omitted a colour is auto-assigned from the palette.
/// </param>
public void Register(string namespacePrefix, string tag, string? ansiColor = null)
{
var color = ansiColor ?? NextAutoColor();
_entries.Add((namespacePrefix, tag, color));
_colorCache[tag] = color;
}
/// <summary>
/// Resolve a full <c>SourceContext</c> string to a (tag, colour) pair.
/// Returns ("Plugin", null) when no registered prefix matches.
/// </summary>
public (string Tag, string? Color) Resolve(string sourceContext)
{
foreach (var (prefix, tag, color) in _entries)
{
if (sourceContext.StartsWith(prefix, StringComparison.Ordinal))
return (tag, color);
}
return ("Plugin", null);
}
public string? GetColor(string tag)
{
_colorCache.TryGetValue(tag, out var c);
return c;
}
private string NextAutoColor()
{
var i = Interlocked.Increment(ref _autoIdx);
return AutoPalette[Math.Abs(i) % AutoPalette.Length];
}
}
@@ -13,11 +13,49 @@ namespace Empostor.Server.Utils
{ {
var shortName = fullName[(fullName.LastIndexOf('.') + 1)..]; var shortName = fullName[(fullName.LastIndexOf('.') + 1)..];
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Source", shortName)); logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Source", shortName));
var module = GetModuleTag(fullName);
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Module", module));
} }
else else
{ {
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Source", "Empostor")); logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Source", "Empostor"));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Module", "Empostor"));
} }
} }
private static string GetModuleTag(string sourceContext)
{
if (sourceContext.StartsWith("Empostor.Server.Net.Manager")) return "Auth";
if (sourceContext.StartsWith("Empostor.Server.Service.Auth")) return "Auth";
if (sourceContext.StartsWith("Empostor.Server.Net.State")) return "Game";
if (sourceContext.StartsWith("Empostor.Server.Net.Inner")) return "Game";
if (sourceContext.StartsWith("Empostor.Server.Net.Messages")) return "Game";
if (sourceContext.StartsWith("Empostor.Server.Net.Factories")) return "Game";
if (sourceContext.StartsWith("Empostor.Server.Service.Admin.Reactor")) return "Reactor";
if (sourceContext.StartsWith("Empostor.Server.Service.Admin.Ban")) return "Ban";
if (sourceContext.StartsWith("Empostor.Server.Service.Admin.Chat")) return "Chat";
if (sourceContext.StartsWith("Empostor.Server.Service.Admin.Report")) return "Admin";
if (sourceContext.StartsWith("Empostor.Server.Service.Admin")) return "Admin";
if (sourceContext.StartsWith("Empostor.Server.Service.Api")) return "Discord";
if (sourceContext.StartsWith("Empostor.Server.Service.Stat")) return "Stat";
if (sourceContext.StartsWith("Empostor.Server.Http")) return "Admin";
if (sourceContext.StartsWith("Empostor.Server.Commands")) return "Cmd";
if (sourceContext.StartsWith("Empostor.Server.Plugins")) return "Plugin";
if (sourceContext.StartsWith("Empostor.Plugin."))
{
var (tag, _) = ModuleTagRegistry.Instance.Resolve(sourceContext);
return tag;
}
return "Empostor";
}
} }
} }
+172
View File
@@ -0,0 +1,172 @@
using System;
using System.IO;
using System.Linq;
using Empostor.Api.Config;
using Empostor.Api.Utils;
using Microsoft.Extensions.Configuration;
namespace Empostor.Server.Utils;
internal static class StartupBanner
{
private const string Bold = "\x1b[1m";
private const string Dim = "\x1b[2m";
private const string Reset = "\x1b[0m";
private const string Cyan = "\x1b[96m";
private const string Green = "\x1b[92m";
private const string Yellow = "\x1b[93m";
private const string Red = "\x1b[91m";
private const string White = "\x1b[97m";
private const string Gray = "\x1b[90m";
private const string BoxH = "─";
private const string BoxV = "│";
private const string BoxTL = "┌";
private const string BoxTR = "┐";
private const string BoxBL = "└";
private const string BoxBR = "┘";
private const string BoxML = "├";
private const string BoxMR = "┤";
private static readonly bool UseColor = !Console.IsOutputRedirected;
public static void Print(IConfiguration configuration)
{
var prevEncoding = Console.OutputEncoding;
try { Console.OutputEncoding = System.Text.Encoding.UTF8; }
catch { /* best-effort */ }
try
{
PrintInternal(configuration);
}
finally
{
try { Console.OutputEncoding = prevEncoding; }
catch { /* ignore */ }
}
}
private static void PrintInternal(IConfiguration configuration)
{
var server = configuration.GetSection(ServerConfig.Section).Get<ServerConfig>() ?? new ServerConfig();
var http = configuration.GetSection(HttpServerConfig.Section).Get<HttpServerConfig>() ?? new HttpServerConfig();
var anticheat = configuration.GetSection(AntiCheatConfig.Section).Get<AntiCheatConfig>() ?? new AntiCheatConfig();
var logLevel = configuration.GetValue<string>("Serilog:MinimumLevel") ?? "Information";
var logPath = Path.Combine(Directory.GetCurrentDirectory(), "Log", $"empostor-{DateTime.Now:yyyy-MM-dd}.log");
var version = DotnetUtils.Version;
string[] labelTexts = { "Public IP", "Listen Port", "HTTP API", "Admin Panel", "Port Pool", "AntiCheat", "Log Level", "Log File" };
var maxLabel = labelTexts.Max(t => t.Length) + 2;
var width = 62;
using var sw = new StringWriter();
sw.WriteLine(C(BoxTL + new string(BoxH[0], width - 2) + BoxTR, Cyan));
Center(sw, width, Bold + White + "Empostor" + Reset + C(" v" + version, Cyan));
Center(sw, width, C("Among Us Private Server", Gray));
sw.WriteLine(C(BoxML + new string(BoxH[0], width - 2) + BoxMR, Cyan));
Row(sw, width, maxLabel, "Public IP", server.PublicIp);
Row(sw, width, maxLabel, "Listen Port", $"{server.ListenPort} (UDP)");
Row(sw, width, maxLabel, "HTTP API", $"http://{http.ListenIp}:{http.ListenPort}");
Row(sw, width, maxLabel, "Admin Panel", $"http://{server.PublicIp}:{http.ListenPort}/admin");
var poolInfo = server.DeltaPortStart > 0 && server.DeltaPortEnd >= server.DeltaPortStart
? $"{server.DeltaPortStart}-{server.DeltaPortEnd} ({server.DeltaPortEnd - server.DeltaPortStart + 1} ports)"
: C("disabled", Yellow);
Row(sw, width, maxLabel, "Port Pool", poolInfo);
Row(sw, width, maxLabel, "AntiCheat", anticheat.Enabled ? C("✔ Enabled", Green) : C("✗ Disabled", Red));
Row(sw, width, maxLabel, "Log Level", logLevel);
Row(sw, width, maxLabel, "Log File", TrimPath(logPath, width - maxLabel - 5));
sw.WriteLine(C(BoxBL + new string(BoxH[0], width - 2) + BoxBR, Cyan));
Console.WriteLine(sw.ToString());
}
private static void Row(StringWriter sw, int width, int labelW, string label, string value)
{
sw.Write(C(BoxV, Cyan));
sw.Write(' ');
sw.Write(C(PadRight(label, labelW), White));
sw.Write(C(BoxV, Cyan));
sw.Write(' ');
sw.Write(C(value, Gray));
var used = 4 + labelW + VisibleLength(value);
var remain = width - 2 - used;
if (remain > 0)
{
sw.Write(new string(' ', remain));
}
sw.WriteLine(C(BoxV, Cyan));
}
private static void Center(StringWriter sw, int width, string text)
{
var vis = VisibleLength(text);
var pad = (width - vis - 2) / 2;
sw.Write(C(BoxV, Cyan));
if (pad > 0)
{
sw.Write(new string(' ', pad));
}
sw.Write(text);
var right = width - vis - pad - 2;
if (right > 0)
{
sw.Write(new string(' ', right));
}
sw.WriteLine(C(BoxV, Cyan));
}
private static string C(string text, string ansi) =>
UseColor ? $"{ansi}{text}{Reset}" : text;
private static int VisibleLength(string text)
{
if (!UseColor)
{
return text.Length;
}
var len = 0;
var inEscape = false;
foreach (var ch in text)
{
if (ch == '\x1b') { inEscape = true; continue; }
if (inEscape) { if (ch == 'm') { inEscape = false; } continue; }
len++;
}
return len;
}
private static string PadRight(string text, int width)
{
var vis = VisibleLength(text);
return text + new string(' ', Math.Max(0, width - vis));
}
private static string TrimPath(string path, int maxLen)
{
if (maxLen < 5)
{
return path;
}
if (path.Length <= maxLen)
{
return path;
}
return "…" + path[^(maxLen - 1)..];
}
}
@@ -786,7 +786,7 @@ public class DtlsConnectionListener : UdpConnectionListener
await SendHelloVerifyRequest(peerAddress, outgoingSequence, record.Epoch, recordProtection, peer.ProtocolVersion); await SendHelloVerifyRequest(peerAddress, outgoingSequence, record.Epoch, recordProtection, peer.ProtocolVersion);
return true; return true;
} }
// Client is initiating a brand new connection. We need // Client is initiating a brand new connection. We need
// to destroy the existing connection and establish a // to destroy the existing connection and establish a
// new session. // new session.
@@ -881,7 +881,7 @@ public class DtlsConnectionListener : UdpConnectionListener
serverHelloHandshake.FragmentLength = serverHelloHandshake.Length; serverHelloHandshake.FragmentLength = serverHelloHandshake.Length;
var maxCertFragmentSize = peer.Session.Version == 0 ? MaxCertFragmentSizeV0 : MaxCertFragmentSizeV1; var maxCertFragmentSize = peer.Session.Version == 0 ? MaxCertFragmentSizeV0 : MaxCertFragmentSizeV1;
/*var certificateData = encodedCertificate; /*var certificateData = encodedCertificate;
var initialCertPadding = Record.Size + Handshake.Handshake.Size + serverHello.Size + Handshake.Handshake.Size; var initialCertPadding = Record.Size + Handshake.Handshake.Size + serverHello.Size + Handshake.Handshake.Size;
var certInitialFragmentSize = Math.Min(certificateData.Length, maxCertFragmentSize - initialCertPadding); var certInitialFragmentSize = Math.Min(certificateData.Length, maxCertFragmentSize - initialCertPadding);
@@ -1,4 +1,4 @@
using System; using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
@@ -165,7 +165,7 @@ public class UPnPHelper : IDisposable
var typeNode = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr); var typeNode = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr);
if (typeNode?.Value == null) if (typeNode?.Value == null)
return false; return false;
if (!typeNode.Value.Contains("InternetGatewayDevice")) if (!typeNode.Value.Contains("InternetGatewayDevice"))
return false; return false;
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
@@ -34,7 +34,7 @@ public class UdpConnectionListener : NetworkConnectionListener
/// </summary> /// </summary>
public AcceptConnectionCheck AcceptConnection; public AcceptConnectionCheck AcceptConnection;
#nullable enable #nullable enable
/// <summary> /// <summary>
/// Creates a new UdpConnectionListener for the given <see cref="IPAddress" />, port and <see cref="IPMode" />. /// Creates a new UdpConnectionListener for the given <see cref="IPAddress" />, port and <see cref="IPMode" />.
/// </summary> /// </summary>
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
@@ -11,7 +11,7 @@ public class UdpConnectionRateLimit : IDisposable
{ {
// Allow burst to 5 connections. // Allow burst to 5 connections.
// Decrease by 1 every second. // Decrease by 1 every second.
public int MaxConnections { get; private set; } = 5; public int MaxConnections { get; private set; } = 5;
public int FalloffMs { get; private set; } = 1000; public int FalloffMs { get; private set; } = 1000;
private static readonly ILogger Logger = Log.ForContext<UdpConnectionRateLimit>(); private static readonly ILogger Logger = Log.ForContext<UdpConnectionRateLimit>();
@@ -65,7 +65,7 @@ public class UdpConnectionRateLimit : IDisposable
public bool IsAllowed(IPAddress key) public bool IsAllowed(IPAddress key)
{ {
if (_connectionCount.TryGetValue(key, out var value) && value >= MaxConnections) if (_connectionCount.TryGetValue(key, out var value) && value >= MaxConnections)
return false; return false;
_connectionCount.AddOrUpdate(key, _ => 1, (_, i) => i + 1); _connectionCount.AddOrUpdate(key, _ => 1, (_, i) => i + 1);