From 6248d3c0ccf5acbe5d26723ca4b5601089247856 Mon Sep 17 00:00:00 2001 From: linmeideli <2558527373@qq.com> Date: Sat, 4 Jul 2026 13:41:21 +0800 Subject: [PATCH] Admin Client Details Add --- .../packages.lock.json | 3 +- src/Empostor.Api/packages.lock.json | 9 +---- src/Empostor.Plugin.Chat/ChatService.cs | 34 ++++++++++++------- src/Empostor.Server/Http/AdminController.cs | 26 +++++++++++--- .../Net/Inner/Objects/InnerPlayerControl.cs | 16 ++++----- .../Net/Manager/ClientManager.cs | 19 +++++++---- .../Net/State/Game.Incoming.cs | 14 ++++++-- src/Empostor.Server/Pages/AdminStrings.json | 1 + .../Admin/Reactor/ReactorHandshakeListener.cs | 20 +++++++++-- .../Utils/IpGeolocationService.cs | 2 +- src/Empostor.Server/Utils/LanguageHelper.cs | 28 +++++++++++++++ src/Empostor.Server/packages.lock.json | 6 ---- 12 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 src/Empostor.Server/Utils/LanguageHelper.cs diff --git a/src/Empostor.Api.Innersloth.Generator/packages.lock.json b/src/Empostor.Api.Innersloth.Generator/packages.lock.json index 5850ca3..fb68fad 100644 --- a/src/Empostor.Api.Innersloth.Generator/packages.lock.json +++ b/src/Empostor.Api.Innersloth.Generator/packages.lock.json @@ -150,7 +150,6 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } } - }, - ".NETStandard,Version=v2.0/win-x64": {} + } } } \ No newline at end of file diff --git a/src/Empostor.Api/packages.lock.json b/src/Empostor.Api/packages.lock.json index 049e555..9d991b4 100644 --- a/src/Empostor.Api/packages.lock.json +++ b/src/Empostor.Api/packages.lock.json @@ -34,12 +34,6 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[8.0.28, )", - "resolved": "8.0.28", - "contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ==" - }, "StyleCop.Analyzers": { "type": "Direct", "requested": "[1.2.0-beta.556, )", @@ -145,7 +139,6 @@ "NextFast.Hazel.Abstractions": { "type": "Project" } - }, - "net8.0/win-x64": {} + } } } \ No newline at end of file diff --git a/src/Empostor.Plugin.Chat/ChatService.cs b/src/Empostor.Plugin.Chat/ChatService.cs index c31667e..6d944bf 100644 --- a/src/Empostor.Plugin.Chat/ChatService.cs +++ b/src/Empostor.Plugin.Chat/ChatService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.Json; +using Empostor.Api; using Empostor.Api.Events.Player; using Microsoft.Extensions.Logging; @@ -21,25 +22,34 @@ public sealed class ChatService public void HandleChatMessage(IPlayerChatEvent e) { - var playerInfo = e.PlayerControl.PlayerInfo; - var playerName = playerInfo?.PlayerName ?? "?"; - var ownerId = e.PlayerControl.OwnerId; + var client = e.ClientPlayer.Client; + var clientId = client.Id; + var playerName = client.Name; + var slot = e.ClientPlayer.Character?.PlayerId ?? 0; + var gameCode = Api.Innersloth.GameCodeParser.IntToGameName(e.Game.Code); - if (e.ClientPlayer.Character == e.PlayerControl) + int channel; + string channelName; + if (e.IsCancelled) { - _logger.LogInformation( - "[{GameCode}] {PlayerName} ({PlayerId}): {Message}", - e.Game.Code, playerName, ownerId, e.Message); + channel = 0; + channelName = "Canceled"; + } + else if (!e.SendToAllPlayers) + { + channel = 255; + channelName = "Command"; } else { - var senderName = e.ClientPlayer.Character?.PlayerInfo?.PlayerName ?? "?"; - var senderId = e.ClientPlayer.Client.Id; - _logger.LogInformation( - "[{GameCode}] {SenderName} ({SenderId}) on behalf of {PlayerName} ({PlayerId}): {Message}", - e.Game.Code, senderName, senderId, playerName, ownerId, e.Message); + channel = -1; + channelName = "Public"; } + _logger.LogInformation( + "{Code} - [{ClientId}][{Name}][{Slot}] => [{Channel}]{ChannelName}: [{Message}]", + gameCode, clientId, playerName, slot, channel, channelName, e.Message); + var isHost = e.ClientPlayer.IsHost; var maxLength = isHost ? _config.HostMaxMessageLength : _config.PlayerMaxMessageLength; diff --git a/src/Empostor.Server/Http/AdminController.cs b/src/Empostor.Server/Http/AdminController.cs index 072b955..7f7b02d 100644 --- a/src/Empostor.Server/Http/AdminController.cs +++ b/src/Empostor.Server/Http/AdminController.cs @@ -20,6 +20,7 @@ using Empostor.Server.Service.Admin.Reactor; using Empostor.Server.Service.Admin.Report; using Empostor.Server.Service.Api; using Empostor.Server.Service.Stat; +using Empostor.Server.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -48,6 +49,7 @@ namespace Empostor.Server.Http private readonly PlayerStatsConfig _statsConfig; private readonly ChatFilterStore _chatFilter; private readonly DiscordWebhookStore _discordWebhook; + private readonly IpGeolocationService _ipGeo; private readonly string _passwordHash; private static readonly ConcurrentDictionary _loginFailures = new(); @@ -62,7 +64,8 @@ namespace Empostor.Server.Http PlayerStatsStore playerStats, IOptions statsConfig, ChatFilterStore chatFilter, - DiscordWebhookStore discordWebhook) + DiscordWebhookStore discordWebhook, + IpGeolocationService ipGeo) { _logger = logger; _gameManager = gameManager; @@ -75,6 +78,7 @@ namespace Empostor.Server.Http _statsConfig = statsConfig.Value; _chatFilter = chatFilter; _discordWebhook = discordWebhook; + _ipGeo = ipGeo; _passwordHash = ComputeHash(_config.Password); if (string.IsNullOrEmpty(_config.Password) @@ -229,14 +233,23 @@ namespace Empostor.Server.Http } [HttpGet("/api/admin/clients")] - public IActionResult GetClients() + public async Task GetClients() { if (!IsAuthenticated()) { return Unauthorized(); } - return Ok(_clientManager.Clients.Select(CSnap)); + var clients = _clientManager.Clients.ToList(); + var snaps = new List(clients.Count); + foreach (var c in clients) + { + var ip = c.Connection?.EndPoint?.Address; + var location = await _ipGeo.GetLocationAsync(ip); + snaps.Add(CSnap(c, location)); + } + + return Ok(snaps); } [HttpGet("/api/admin/bans")] @@ -803,9 +816,10 @@ namespace Empostor.Server.Http }).ToList(), }; - private static object CSnap(IClient c) + private static object CSnap(IClient c, string location = "") { var reactor = c.GetReactorMods(); + var level = c.Player?.Character?.PlayerInfo?.PlayerLevel; return new { id = c.Id, @@ -813,9 +827,13 @@ namespace Empostor.Server.Http friendCode = c.FriendCode ?? "—", gameVersion = c.GameVersion.ToString(), platform = c.PlatformSpecificData?.Platform.ToString() ?? "Unknown", + platformName = c.PlatformSpecificData?.PlatformName ?? "", + language = LanguageHelper.GetDisplayName(c.Language), + level = level is null or 0 or uint.MaxValue ? (int?)null : (int)level.Value, inGame = c.Player != null, gameCode = c.Player != null ? GameCodeParser.IntToGameName(c.Player.Game.Code) : "—", ip = c.Connection?.EndPoint?.Address?.ToString() ?? "—", + location = location, reactor = reactor == null ? null : new { protocolVersion = reactor.ProtocolVersion, diff --git a/src/Empostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Empostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index 6b19435..5396d05 100644 --- a/src/Empostor.Server/Net/Inner/Objects/InnerPlayerControl.cs +++ b/src/Empostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -1006,6 +1006,12 @@ namespace Empostor.Server.Net.Inner.Objects } else { + 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); + } + PlayerInfo.PlayerLevel = level; } @@ -1146,10 +1152,6 @@ namespace Empostor.Server.Net.Inner.Objects private async ValueTask HandleSendChat(ClientPlayer sender, string message) { - var clientId = sender.Client.Id; - var playerName = sender.Client.Name; - var slot = sender.Character?.PlayerId ?? 0; - if (message.StartsWith("#")) { var ctx = new CommandContext @@ -1166,10 +1168,7 @@ namespace Empostor.Server.Net.Inner.Objects }; var handled = await _commandService.TryHandleAsync(ctx); if (handled) - { - _logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [255]Command:\n [{Message}]", clientId, playerName, slot, message); return false; - } } var @event = new PlayerChatEvent(Game, sender, this, message); @@ -1181,12 +1180,10 @@ namespace Empostor.Server.Net.Inner.Objects await _eventManager.CallAsync(@event); if (@event.IsCancelled) { - _logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [0]Canceled:\n [{Message}]", clientId, playerName, slot, message); return false; } else if (@event.SendToAllPlayers == false) { - _logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [255]Command:\n [{Message}]", clientId, playerName, slot, message); if (Game.Host != null) { await SendChatToPlayerAsync(message, Game.Host.Character); @@ -1196,7 +1193,6 @@ namespace Empostor.Server.Net.Inner.Objects } else { - _logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [-1]Public:\n [{Message}]", clientId, playerName, slot, message); return true; } } diff --git a/src/Empostor.Server/Net/Manager/ClientManager.cs b/src/Empostor.Server/Net/Manager/ClientManager.cs index 7002f87..1e0420a 100644 --- a/src/Empostor.Server/Net/Manager/ClientManager.cs +++ b/src/Empostor.Server/Net/Manager/ClientManager.cs @@ -13,6 +13,7 @@ using Empostor.Api.Net.Manager; using Empostor.Server.Events.Client; using Empostor.Server.Net.Factories; using Empostor.Api.Service; +using Empostor.Server.Service.Admin.Reactor; using Empostor.Server.Service.Auth; using Empostor.Server.Utils; using Microsoft.Extensions.Logging; @@ -143,6 +144,12 @@ namespace Empostor.Server.Net.Manager UserAuthInfo? authInfo = null; var clientIp = connection.EndPoint?.Address; var location = await _ipGeo.GetLocationAsync(clientIp); + var lang = LanguageHelper.GetDisplayName(language); + 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}"))}]" + : string.Empty; // Primary: match by delta port (nonce) if (deltaPort > 0) @@ -177,8 +184,8 @@ namespace Empostor.Server.Net.Manager } _logger.LogInformation( - "Client [{Id}]{Name} from port {Port} {Location}is authorized as {FriendCode} {HashPuid}", - id, name, deltaPort, location, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId)); + "Client [{Id}]{Name} from port {Port} {Location}[{Lang}] [{Platform}] is authorized as {FriendCode} {HashPuid}{Reactor}", + id, name, deltaPort, location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr); } else { @@ -196,14 +203,14 @@ namespace Empostor.Server.Net.Manager { friendCode = authInfo.FriendCode; _logger.LogInformation( - "Client [{Id}]{Name} from {Ip} {Location}is authorized as {FriendCode} {HashPuid}", - id, name, NormalizeIp(clientIp), location, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId)); + "Client [{Id}]{Name} from {Ip} {Location}[{Lang}] [{Platform}] is authorized as {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})", - id, name, deltaPort, NormalizeIp(clientIp), location); + "Client [{Id}]{Name}: no auth info found (port={Port}, ip={Ip} {Location}) [{Lang}] [{Platform}]{Reactor}", + id, name, deltaPort, NormalizeIp(clientIp), location, lang, platformStr, reactorStr); } } diff --git a/src/Empostor.Server/Net/State/Game.Incoming.cs b/src/Empostor.Server/Net/State/Game.Incoming.cs index 924483b..4d5c12c 100644 --- a/src/Empostor.Server/Net/State/Game.Incoming.cs +++ b/src/Empostor.Server/Net/State/Game.Incoming.cs @@ -6,6 +6,8 @@ using Empostor.Api.Games; using Empostor.Api.Net; using Next.Hazel; using Empostor.Server.Events; +using Empostor.Server.Service.Admin.Reactor; +using Empostor.Server.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -127,9 +129,17 @@ namespace Empostor.Server.Net.State 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}"))}]" + : string.Empty; - _logger.LogInformation("{Code} - Player {Name} ({HashPuid}) ({Id}) is joining from ({Ip} {Location}:{RemotePort}) with v{Version}, Authority:{Authority}, port: {DeltaPort}", - Code, sender.Client.Name, hashPuid, sender.Client.Id, ipStr, location, remotePort, version, authority, deltaPort); + _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); if (isNew) { diff --git a/src/Empostor.Server/Pages/AdminStrings.json b/src/Empostor.Server/Pages/AdminStrings.json index 0a9ed95..55fd290 100644 --- a/src/Empostor.Server/Pages/AdminStrings.json +++ b/src/Empostor.Server/Pages/AdminStrings.json @@ -152,6 +152,7 @@ "clients.version_label": "Version", "clients.platform_label": "Platform", "clients.language_label": "Language", + "clients.level_label": "Level", "clients.ingame_label": "In Game", "reactor.required_yes": "Yes", diff --git a/src/Empostor.Server/Service/Admin/Reactor/ReactorHandshakeListener.cs b/src/Empostor.Server/Service/Admin/Reactor/ReactorHandshakeListener.cs index 1a397e1..3cda876 100644 --- a/src/Empostor.Server/Service/Admin/Reactor/ReactorHandshakeListener.cs +++ b/src/Empostor.Server/Service/Admin/Reactor/ReactorHandshakeListener.cs @@ -15,7 +15,7 @@ namespace Empostor.Server.Service.Admin.Reactor internal sealed class ReactorHandshakeListener : IEventListener { // "reactor" in ASCII (7 bytes), packed as upper 56 bits of a uint64 in little-endian - private const ulong ReactorMagic = 0x726f7463656172; + private const ulong ReactorMagic = 0x72656163746f72; private readonly ILogger _logger; @@ -30,7 +30,9 @@ namespace Empostor.Server.Service.Admin.Reactor try { - // Skip known Among Us handshake fields to reach the Reactor suffix + // HandshakeC2S.Deserialize already consumed the AU handshake, + // so we must seek back to the beginning before skipping. + reader.Seek(0); if (!SkipAuHandshake(reader)) { return; @@ -49,7 +51,7 @@ namespace Empostor.Server.Service.Admin.Reactor // Read mod list for protocol V3+ var mods = Array.Empty(); - if (protocolVersion >= 3) + if (protocolVersion >= 2) // ReactorProtocolVersion.V3 = 2 { mods = ReadModList(reader); } @@ -123,6 +125,18 @@ namespace Empostor.Server.Service.Admin.Reactor return false; } + // ProductUserId (Hazel string) — may be absent for older clients + if (CanRead(reader, 1)) + { + TrySkipString(reader); + } + + // CrossplayFlags (uint32, 4 bytes) — may be absent for older clients + if (CanRead(reader, 4)) + { + reader.ReadUInt32(); + } + return true; } diff --git a/src/Empostor.Server/Utils/IpGeolocationService.cs b/src/Empostor.Server/Utils/IpGeolocationService.cs index d569a7e..128e900 100644 --- a/src/Empostor.Server/Utils/IpGeolocationService.cs +++ b/src/Empostor.Server/Utils/IpGeolocationService.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace Empostor.Server.Utils { - internal class IpGeolocationService + public class IpGeolocationService { private const string ApiUrl = "http://ip-api.com/json/{0}?lang=zh-CN&fields=status,country,regionName,city"; private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(24); diff --git a/src/Empostor.Server/Utils/LanguageHelper.cs b/src/Empostor.Server/Utils/LanguageHelper.cs new file mode 100644 index 0000000..2a3bec9 --- /dev/null +++ b/src/Empostor.Server/Utils/LanguageHelper.cs @@ -0,0 +1,28 @@ +using Empostor.Api.Innersloth; + +namespace Empostor.Server.Utils +{ + /// + /// Covers both the base Among Us SupportLangs (0-15) + LocalizeUs / ExtendedLangs (16-22). + /// + internal static class LanguageHelper + { + private static readonly string[] ExtendedLangs = { "Polish", "Turkish", "Swedish", "Lithuanian", "Czech", "華夏(文言文)", "Greek" }; + + public static string GetDisplayName(Language lang) + { + var val = (int)lang; + if (val >= 0 && val <= 15) + { + return $"{lang}({val})"; + } + + if (val >= 16 && val < 16 + ExtendedLangs.Length) + { + return $"{ExtendedLangs[val - 16]}({val})"; + } + + return $"Unknown({val})"; + } + } +} diff --git a/src/Empostor.Server/packages.lock.json b/src/Empostor.Server/packages.lock.json index 4588685..9d36848 100644 --- a/src/Empostor.Server/packages.lock.json +++ b/src/Empostor.Server/packages.lock.json @@ -58,12 +58,6 @@ "resolved": "8.0.1", "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[8.0.28, )", - "resolved": "8.0.28", - "contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ==" - }, "Serilog.Extensions.Hosting": { "type": "Direct", "requested": "[8.0.0, )",