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 string NikoApiKey { get; set; } = "";
public string NikoApiKey { get; set; } = string.Empty;
public string NikoApiBaseUrl { get; set; } = "https://au-verify.niko233.top";
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";
+1 -1
View File
@@ -1,4 +1,4 @@
namespace Empostor.Api.Innersloth
namespace Empostor.Api.Innersloth
{
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 Empostor.Api.Plugins;
using Empostor.Api.Utils;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugin.Chat;
@@ -9,14 +10,15 @@ public sealed class ChatPlugin : PluginBase
{
private readonly ILogger<ChatPlugin> _logger;
public ChatPlugin(ILogger<ChatPlugin> logger)
public ChatPlugin(ILogger<ChatPlugin> logger, IModuleTagRegistry tags)
{
_logger = logger;
tags.Register("Empostor.Plugin.Chat", "Chat", "\x1b[92m");
}
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;
}
+4 -12
View File
@@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Text.Json;
using Empostor.Api;
using Empostor.Api.Events.Player;
using Microsoft.Extensions.Logging;
@@ -22,33 +21,26 @@ public sealed class ChatService
public void HandleChatMessage(IPlayerChatEvent e)
{
var client = e.ClientPlayer.Client;
var clientId = client.Id;
var playerName = client.Name;
var playerName = e.ClientPlayer.Client.Name;
var slot = e.ClientPlayer.Character?.PlayerId ?? 0;
var gameCode = Api.Innersloth.GameCodeParser.IntToGameName(e.Game.Code);
int channel;
string channelName;
if (e.IsCancelled)
{
channel = 0;
channelName = "Canceled";
}
else if (!e.SendToAllPlayers)
{
channel = 255;
channelName = "Command";
}
else
{
channel = -1;
channelName = "Public";
}
_logger.LogInformation(
"{Code} - [{ClientId}][{Name}][{Slot}] => [{Channel}]{ChannelName}: [{Message}]",
gameCode, clientId, playerName, slot, channel, channelName, e.Message);
"✉ {Name} [{Slot}] → {ChannelName}: {Message}",
playerName, slot, channelName, e.Message);
var isHost = e.ClientPlayer.IsHost;
var maxLength = isHost ? _config.HostMaxMessageLength : _config.PlayerMaxMessageLength;
@@ -56,7 +48,7 @@ public sealed class ChatService
if (e.Message.Length > maxLength)
{
_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);
e.PlayerControl.SendChatToPlayerAsync(_config.TooLongMessage, e.PlayerControl);
@@ -12,7 +12,7 @@ public sealed class FixedCodePlugin : PluginBase
public FixedCodePlugin(ILogger<FixedCodePlugin> logger, FixedCodeListener listener)
{
_logger = logger;
_logger = logger;
_listener = listener;
}
@@ -15,7 +15,7 @@ namespace Empostor.Plugins.FriendCodeValidator
private static readonly Regex Pattern =
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",
"abiotic",
@@ -2420,7 +2420,7 @@ namespace Empostor.Plugins.FriendCodeValidator
"zonal",
};
private static readonly HashSet<string> WordsB = new(StringComparer.OrdinalIgnoreCase)
private static readonly HashSet<string> WordsB = new(StringComparer.OrdinalIgnoreCase)
{
"ability",
"absence",
@@ -5402,7 +5402,7 @@ namespace Empostor.Plugins.FriendCodeValidator
// Try every split point: left+right must be (A,B) or (B,A)
for (var i = 1; i < name.Length; i++)
{
var left = name[..i];
var left = name[..i];
var right = name[i..];
if ((WordsA.Contains(left) && WordsB.Contains(right)) ||
(WordsB.Contains(left) && WordsA.Contains(right)))
@@ -14,7 +14,7 @@ public sealed class TitleEventListener : IEventListener
public TitleEventListener(ILogger<TitleEventListener> logger, TitleStore store)
{
_logger = logger;
_store = store;
_store = store;
}
[EventListener]
@@ -51,7 +51,7 @@ public sealed class WelcomeMessageService
if (colon < 1) continue;
var lang = line[..colon].Trim();
var msg = line[(colon + 1)..].Trim();
var msg = line[(colon + 1)..].Trim();
if (!string.IsNullOrEmpty(msg))
_templates[lang] = msg;
}
@@ -71,7 +71,7 @@ public sealed class WelcomeMessageService
return template
.Replace("{Name}", name, StringComparison.OrdinalIgnoreCase)
.Replace("{FriendCode}", friendCode ?? "None", StringComparison.OrdinalIgnoreCase)
.Replace("{GameCode}", gameCode, StringComparison.OrdinalIgnoreCase);
.Replace("{GameCode}", gameCode, StringComparison.OrdinalIgnoreCase);
}
private const string DefaultContent = """
@@ -45,7 +45,9 @@ public sealed class CommandService
public async ValueTask<bool> TryHandleAsync(CommandContext ctx)
{
if (!_commands.TryGetValue(ctx.Name, out var command))
{
return false;
}
try
{
@@ -32,7 +32,9 @@ public sealed class MaxCommand : ICommand
var msg = ctx.GetString("command.max.set").Format(count).Get();
if (count > 15)
{
msg += "\n" + ctx.GetString("command.max.warning");
}
await ctx.PlayerControl.SendChatToPlayerAsync(msg, ctx.PlayerControl);
return true;
+1 -1
View File
@@ -827,7 +827,7 @@ namespace Empostor.Server.Http
friendCode = c.FriendCode ?? "—",
gameVersion = c.GameVersion.ToString(),
platform = c.PlatformSpecificData?.Platform.ToString() ?? "Unknown",
platformName = c.PlatformSpecificData?.PlatformName ?? "",
platformName = c.PlatformSpecificData?.PlatformName ?? string.Empty,
language = LanguageHelper.GetDisplayName(c.Language),
level = level is null or 0 or uint.MaxValue ? (int?)null : (int)level.Value,
inGame = c.Player != null,
@@ -18,9 +18,14 @@ internal static class AdminTemplateDefaults
var loginPath = Path.Combine(PagesDir, "login.html");
var adminPath = Path.Combine(PagesDir, "admin.html");
if (!File.Exists(loginPath))
{
File.WriteAllText(loginPath, LoginHtml);
}
if (!File.Exists(adminPath))
{
File.WriteAllText(adminPath, AdminHtml);
}
}
catch
{
+10 -10
View File
@@ -85,25 +85,25 @@ public sealed class MonitorController : ControllerBase
public sealed class MonitorStatus
{
[JsonPropertyName("status")]
public string Status { get; set; } = "";
public string Status { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = "";
public string Timestamp { get; set; } = string.Empty;
[JsonPropertyName("uptime_seconds")]
public long UptimeSeconds { get; set; }
[JsonPropertyName("uptime_display")]
public string UptimeDisplay { get; set; } = "";
public string UptimeDisplay { get; set; } = string.Empty;
[JsonPropertyName("version")]
public string Version { get; set; } = "";
public string Version { get; set; } = string.Empty;
[JsonPropertyName("runtime")]
public string Runtime { get; set; } = "";
public string Runtime { get; set; } = string.Empty;
[JsonPropertyName("platform")]
public string Platform { get; set; } = "";
public string Platform { get; set; } = string.Empty;
[JsonPropertyName("processors")]
public int Processors { get; set; }
@@ -127,17 +127,17 @@ public sealed class MonitorStatus
public sealed class MonitorGame
{
[JsonPropertyName("code")]
public string Code { get; set; } = "";
public string Code { get; set; } = string.Empty;
[JsonPropertyName("state")]
public string State { get; set; } = "";
public string State { get; set; } = string.Empty;
[JsonPropertyName("map")]
public string Map { get; set; } = "";
public string Map { get; set; } = string.Empty;
[JsonPropertyName("player_count")]
public int PlayerCount { get; set; }
[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()
{
if (!System.IO.File.Exists(PrivacyFile))
{
return Content(DefaultPrivacyHtml(), "text/html; charset=utf-8");
}
var html = System.IO.File.ReadAllText(PrivacyFile);
return Content(html, "text/html; charset=utf-8");
@@ -43,15 +45,17 @@ public sealed class PrivacyController : ControllerBase
try
{
var doc = System.Text.Json.JsonDocument.Parse(body);
var content = doc.RootElement.GetProperty("content").GetString() ?? "";
var token = doc.RootElement.GetProperty("token").GetString() ?? "";
var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty;
var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty;
var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN")
?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN")
?? "empostor";
if (token != adminToken)
{
return Unauthorized(new { error = "Invalid token." });
}
System.IO.File.WriteAllText(PrivacyFile, content);
_logger.LogInformation("PrivacyControllerPrivacy policy updated.");
@@ -65,15 +69,24 @@ public sealed class PrivacyController : ControllerBase
private void EnsureFile()
{
if (_initialized) return;
if (_initialized)
{
return;
}
lock (_initLock)
{
if (_initialized) return;
if (_initialized)
{
return;
}
_initialized = true;
if (!Directory.Exists(PagesDir))
{
Directory.CreateDirectory(PagesDir);
}
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
{
ApiKey = _authApiConfig.NikoApiKey,
FriendCode = "",
FriendCode = string.Empty,
});
var putReq = new HttpRequestMessage(HttpMethod.Put, apiUrl)
@@ -736,7 +736,7 @@ public sealed class TokenController : ControllerBase
public required string ApiKey { get; init; }
[JsonPropertyName("FriendCode")]
public string FriendCode { get; init; } = "";
public string FriendCode { get; init; } = string.Empty;
}
private sealed class NikoCreateResponse
+2 -1
View File
@@ -50,7 +50,8 @@ namespace Empostor.Server.Net
if (Player != null && Player.IsHost)
{
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch
{
CheatingHostMode.Always => true,
CheatingHostMode.IfRequested => GameVersion.HasDisableServerAuthorityFlag,
CheatingHostMode.Never => false,
@@ -1008,8 +1008,8 @@ namespace Empostor.Server.Net.Inner.Objects
{
if (PlayerInfo.PlayerLevel != level)
{
_logger.LogInformation("{Code} - Player {Name} ({Id}) level changed {Old} -> {New}",
_game.Code, sender.Client.Name, sender.Client.Id, PlayerInfo.PlayerLevel, level);
_logger.LogInformation("★ {Name} ({Id}) | Level {Old} → {New}",
sender.Client.Name, sender.Client.Id, PlayerInfo.PlayerLevel, level);
}
PlayerInfo.PlayerLevel = level;
@@ -1168,7 +1168,9 @@ namespace Empostor.Server.Net.Inner.Objects
};
var handled = await _commandService.TryHandleAsync(ctx);
if (handled)
{
return false;
}
}
var @event = new PlayerChatEvent(Game, sender, this, message);
@@ -109,7 +109,7 @@ namespace Empostor.Server.Net.Manager
if (versionCompare == ICompatibilityManager.VersionCompareResult.ServerTooOld
&& _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
|| platformSpecificData == null)
@@ -134,7 +134,7 @@ namespace Empostor.Server.Net.Manager
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; }
@@ -148,7 +148,7 @@ namespace Empostor.Server.Net.Manager
var platformStr = platformSpecificData?.Platform.ToString() ?? "Unknown";
var reactorMods = connection.GetReactorMods();
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;
// Primary: match by delta port (nonce)
@@ -184,13 +184,13 @@ namespace Empostor.Server.Net.Manager
}
_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);
}
else
{
_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);
}
}
@@ -203,13 +203,13 @@ namespace Empostor.Server.Net.Manager
{
friendCode = authInfo.FriendCode;
_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);
}
else
{
_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);
}
}
@@ -220,7 +220,7 @@ namespace Empostor.Server.Net.Manager
client.DeltaPort = deltaPort;
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);
await _eventManager.CallAsync(new ClientConnectedEvent(connection, client));
}
+1 -1
View File
@@ -206,7 +206,7 @@ namespace Empostor.Server.Net.State
continue;
}
_logger.LogWarning("Couldn't find spawnable object {0}.", objectId);
_logger.LogWarning("Couldn't find spawnable object {ObjectId}", objectId);
break;
}
+7 -12
View File
@@ -72,7 +72,7 @@ namespace Empostor.Server.Net.State
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))
{
WriteKickPlayerMessage(kickMsg, false, playerId, isBan);
@@ -121,25 +121,20 @@ namespace Empostor.Server.Net.State
private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew)
{
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 version = client.GameVersion.ToString();
var hashPuid = client.ProductUserId?.Length >= 9 ? client.ProductUserId[..9] : client.ProductUserId ?? "0";
var deltaPort = client.DeltaPort;
var location = await _ipGeo.GetLocationAsync(clientIp);
var lang = LanguageHelper.GetDisplayName(client.Language);
var platform = client.PlatformSpecificData?.Platform.ToString() ?? "Unknown";
var platformName = client.PlatformSpecificData?.PlatformName;
var platformStr = string.IsNullOrEmpty(platformName) ? platform : $"{platform}/{platformName}";
var reactorMods = client.GetReactorMods();
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;
_logger.LogInformation("{Code} - Player {Name} ({HashPuid}) ({Id}) [{Lang}] [{Platform}] is joining from ({Ip} {Location}:{RemotePort}) with v{Version}, Authority:{Authority}, port: {DeltaPort}{Reactor}",
Code, sender.Client.Name, hashPuid, sender.Client.Id, lang, platformStr, ipStr, location, remotePort, version, authority, deltaPort, reactorStr);
_logger.LogInformation("▶ {Name} ({Id}) joined | {Lang} | {Platform} | v{Version} | Authority:{Authority} | port {DeltaPort}{Reactor}",
sender.Client.Name, sender.Client.Id, lang, platformStr, version, authority, deltaPort, reactorStr);
if (isNew)
{
@@ -229,8 +224,8 @@ namespace Empostor.Server.Net.State
private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew)
{
var authority = sender.Client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false";
_logger.LogInformation("{Code} - Player {Name} ({Id}) is rejoining. Player Authority : {Authority}, port: {DeltaPort}",
Code, sender.Client.Name, sender.Client.Id, authority, sender.Client.DeltaPort);
_logger.LogInformation("↻ {Name} ({Id}) rejoined | Authority:{Authority} | port {DeltaPort}",
sender.Client.Name, sender.Client.Id, authority, sender.Client.DeltaPort);
if (isNew)
{
@@ -239,7 +234,7 @@ namespace Empostor.Server.Net.State
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;
await HandleJoinGameNew(sender, false);
await CheckLimboPlayers();
+4 -4
View File
@@ -33,8 +33,8 @@ namespace Empostor.Server.Net.State
return false;
}
_logger.LogInformation("{Code} - Player {Name} ({Id}) has left for \"{Reason}\" hashpuid : {HashPuid}",
Code, player.Client.Name, playerId, reason,
_logger.LogInformation("◀ {Name} ({Id}) left | {Reason} | {HashPuid}",
player.Client.Name, playerId, reason,
player.Client.ProductUserId?.Length >= 9 ? player.Client.ProductUserId[..9] : player.Client.ProductUserId ?? "0");
if (GameState == GameStates.Starting || GameState == GameStates.Started || GameState == GameStates.NotStarted)
{
@@ -71,7 +71,7 @@ namespace Empostor.Server.Net.State
await Task.Delay(_timeoutConfig.ConnectionTimeout);
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);
}
});
@@ -103,7 +103,7 @@ namespace Empostor.Server.Net.State
}
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)
{
GameState = GameStates.NotStarted;
+1 -5
View File
@@ -16,7 +16,6 @@ using Empostor.Api.Net.Manager;
using Empostor.Api.Net.Messages.S2C;
using Empostor.Server.Events;
using Empostor.Server.Net.Manager;
using Empostor.Server.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -34,7 +33,6 @@ namespace Empostor.Server.Net.State
private readonly ICompatibilityManager _compatibilityManager;
private readonly CompatibilityConfig _compatibilityConfig;
private readonly TimeoutConfig _timeoutConfig;
private readonly IpGeolocationService _ipGeo;
public Game(
ILogger<Game> logger,
@@ -48,8 +46,7 @@ namespace Empostor.Server.Net.State
IEventManager eventManager,
ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig,
IpGeolocationService ipGeo)
IOptions<TimeoutConfig> timeoutConfig)
{
_logger = logger;
_serviceProvider = serviceProvider;
@@ -68,7 +65,6 @@ namespace Empostor.Server.Net.State
_compatibilityManager = compatibilityManager;
_compatibilityConfig = compatibilityConfig.Value;
_timeoutConfig = timeoutConfig.Value;
_ipGeo = ipGeo;
Items = new ConcurrentDictionary<object, object>();
}
+7 -4
View File
@@ -54,13 +54,15 @@ namespace Empostor.Server
private static int Main(string[] args)
{
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>()
.CreateBootstrapLogger();
try
{
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;
}
catch (Exception ex)
@@ -163,6 +165,7 @@ namespace Empostor.Server
});
services.AddSingleton<IpGeolocationService>();
services.AddSingleton<IModuleTagRegistry>(ModuleTagRegistry.Instance);
services.AddSingleton<ICompatibilityManager, CompatibilityManager>();
services.AddSingleton<ClientManager>();
services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>());
@@ -255,12 +258,12 @@ namespace Empostor.Server
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.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(
"Log/empostor-.log",
rollingInterval: RollingInterval.Day,
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,
new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles));
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)..];
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Source", shortName));
var module = GetModuleTag(fullName);
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Module", module));
}
else
{
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);
return true;
}
// Client is initiating a brand new connection. We need
// to destroy the existing connection and establish a
// new session.
@@ -881,7 +881,7 @@ public class DtlsConnectionListener : UdpConnectionListener
serverHelloHandshake.FragmentLength = serverHelloHandshake.Length;
var maxCertFragmentSize = peer.Session.Version == 0 ? MaxCertFragmentSizeV0 : MaxCertFragmentSizeV1;
/*var certificateData = encodedCertificate;
var initialCertPadding = Record.Size + Handshake.Handshake.Size + serverHello.Size + Handshake.Handshake.Size;
var certInitialFragmentSize = Math.Min(certificateData.Length, maxCertFragmentSize - initialCertPadding);
@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
@@ -165,7 +165,7 @@ public class UPnPHelper : IDisposable
var typeNode = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr);
if (typeNode?.Value == null)
return false;
if (!typeNode.Value.Contains("InternetGatewayDevice"))
return false;
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
@@ -34,7 +34,7 @@ public class UdpConnectionListener : NetworkConnectionListener
/// </summary>
public AcceptConnectionCheck AcceptConnection;
#nullable enable
#nullable enable
/// <summary>
/// Creates a new UdpConnectionListener for the given <see cref="IPAddress" />, port and <see cref="IPMode" />.
/// </summary>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
@@ -11,7 +11,7 @@ public class UdpConnectionRateLimit : IDisposable
{
// Allow burst to 5 connections.
// 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;
private static readonly ILogger Logger = Log.ForContext<UdpConnectionRateLimit>();
@@ -65,7 +65,7 @@ public class UdpConnectionRateLimit : IDisposable
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;
_connectionCount.AddOrUpdate(key, _ => 1, (_, i) => i + 1);