diff --git a/marketplace/Empostor.Plugin.Narrator.dll b/marketplace/Empostor.Plugin.Narrator.dll deleted file mode 100644 index 89c6238..0000000 Binary files a/marketplace/Empostor.Plugin.Narrator.dll and /dev/null differ diff --git a/marketplace/Empostor.Plugins.MapVote.dll b/marketplace/Empostor.Plugins.MapVote.dll deleted file mode 100644 index af1a6f3..0000000 Binary files a/marketplace/Empostor.Plugins.MapVote.dll and /dev/null differ diff --git a/marketplace/Empostor.Plugins.PlayerLog.dll b/marketplace/Empostor.Plugins.PlayerLog.dll deleted file mode 100644 index 09a0923..0000000 Binary files a/marketplace/Empostor.Plugins.PlayerLog.dll and /dev/null differ diff --git a/marketplace/Empostor.Plugins.Titles.dll b/marketplace/Empostor.Plugins.Titles.dll deleted file mode 100644 index 4ac04fb..0000000 Binary files a/marketplace/Empostor.Plugins.Titles.dll and /dev/null differ diff --git a/marketplace/plugins.json b/marketplace/plugins.json index 5de94f3..4b2a591 100644 --- a/marketplace/plugins.json +++ b/marketplace/plugins.json @@ -64,19 +64,6 @@ } ] }, - { - "id": "cn.Empostor.titles", - "name": "Custom Title", - "description": "Assigns a title based on their friend code and show in front of their name.", - "author": "HayashiUme", - "versions": [ - { - "version": "1.0.0", - "empostor_version": "2.0.0", - "download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Titles.dll" - } - ] - }, { "id": "cn.hayashiume.playerchannel", "name": "Player Channel", @@ -103,32 +90,6 @@ } ] }, - { - "id": "cn.hayashiume.narrator", - "name": "Narrator", - "description": "AI-powered meeting assistant using DeepSeek. Use #narrator during meetings to get strategic advice. Host commands: #narrator enable/disable/limit.", - "author": "ELinmei", - "versions": [ - { - "version": "1.0.0", - "empostor_version": "2.0.0", - "download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugin.Narrator.dll" - } - ] - }, - { - "id": "cn.hayashiume.mapvote", - "name": "Map Vote", - "description": "using #votemap to choose a map for lobby", - "author": "ELinmei", - "versions": [ - { - "version": "1.0.0", - "empostor_version": "2.0.0", - "download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.MapVote.dll" - } - ] - }, { "id": "cn.hayashiume.message", "name": "Leave a Message", @@ -194,19 +155,6 @@ } ] }, - { - "id": "gg.empostor.playerlog", - "name": "Player Log", - "description": "Logs all player activity (chat, kills, meetings, joins, leaves) and provides an admin panel to view, search, and export logs.", - "author": "Empostor", - "versions": [ - { - "version": "2.0.0", - "empostor_version": "2.0.0", - "download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.PlayerLog.dll" - } - ] - }, { "id": "gg.empostor.monitor", "name": "Monitor", diff --git a/src/Empostor.Api/Net/IClient.cs b/src/Empostor.Api/Net/IClient.cs index 3ec1d34..49fcda8 100644 --- a/src/Empostor.Api/Net/IClient.cs +++ b/src/Empostor.Api/Net/IClient.cs @@ -86,6 +86,11 @@ namespace Empostor.Api.Net /// string? ProductUserId { get; set; } + /// + /// Gets or sets the raw EOS token the player authenticated with. + /// + string? EosToken { get; set; } + ValueTask ReportCheatAsync(CheatContext context, CheatCategory category, string message); [Obsolete("Please use the overload that adds a cheat category")] diff --git a/src/Empostor.Api/Net/IClientPlayer.cs b/src/Empostor.Api/Net/IClientPlayer.cs index d5f9172..c5c9140 100644 --- a/src/Empostor.Api/Net/IClientPlayer.cs +++ b/src/Empostor.Api/Net/IClientPlayer.cs @@ -15,6 +15,11 @@ namespace Empostor.Api.Net /// IClient Client { get; } + /// + /// Gets the EOS token the player authenticated with. + /// + string? EosToken { get; } + /// /// Gets the game where the belongs to. /// diff --git a/src/Empostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs b/src/Empostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs index aedea02..ad9665c 100644 --- a/src/Empostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs +++ b/src/Empostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs @@ -38,6 +38,11 @@ namespace Empostor.Api.Net.Inner.Objects /// DeathReason LastDeathReason { get; } + /// + /// Gets the EOS token the owning player authenticated with. + /// + string? EosToken { get; } + IEnumerable Tasks { get; } DateTimeOffset LastMurder { get; } diff --git a/src/Empostor.Plugin.Narrator/Empostor.Plugin.Narrator.csproj b/src/Empostor.Plugin.Narrator/Empostor.Plugin.Narrator.csproj deleted file mode 100644 index ac43447..0000000 --- a/src/Empostor.Plugin.Narrator/Empostor.Plugin.Narrator.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0 - Empostor.Plugin.Narrator - enable - - - - - - - - diff --git a/src/Empostor.Plugin.Narrator/Localization/en.json b/src/Empostor.Plugin.Narrator/Localization/en.json deleted file mode 100644 index 1a0a40d..0000000 --- a/src/Empostor.Plugin.Narrator/Localization/en.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "command.narrator.description":"Ask the narrator for AI-powered advice during a meeting.", - "command.narrator.usage":"narrator ", - "narrator.meeting_only":"You can only use #narrator during a meeting.", - "narrator.disabled":"The narrator is currently disabled by the host.", - "narrator.status_enabled":"enabled", - "narrator.status_disabled":"disabled", - "narrator.host_status":"Status: {0} | Max uses per player per game: {1}", - "narrator.host_commands":"Host commands: #narrator enable | disable | limit ", - "narrator.host_enabled":"Narrator enabled for this game.", - "narrator.host_disabled":"Narrator disabled for this game.", - "narrator.host_limit_set":"Narrator usage limit set to {0} per player per game.", - "narrator.host_limit_zero":"Narrator usage limit set to 0 (effectively disabled).", - "narrator.host_limit_usage":"Usage: #narrator limit \nExample: #narrator limit 3", - "narrator.usage":"Usage: #narrator \nExample: #narrator I'm being accused but I have a visual task, how do I prove myself?" -} diff --git a/src/Empostor.Plugin.Narrator/Localization/zh-CN.json b/src/Empostor.Plugin.Narrator/Localization/zh-CN.json deleted file mode 100644 index 125b3a3..0000000 --- a/src/Empostor.Plugin.Narrator/Localization/zh-CN.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "command.narrator.description":"在会议中向旁白寻求AI建议。", - "command.narrator.usage":"narrator <你要提的问题>", - "narrator.meeting_only":"只能在会议中使用 #narrator。", - "narrator.disabled":"房主已禁用旁白功能。", - "narrator.status_enabled":"已启用", - "narrator.status_disabled":"已禁用", - "narrator.host_status":"状态:{0} | 每局每人最大次数:{1}", - "narrator.host_commands":"房主指令:#narrator enable | disable | limit <次数>", - "narrator.host_enabled":"旁白已在此局中启用。", - "narrator.host_disabled":"旁白已在此局中禁用。", - "narrator.host_limit_set":"旁白使用次数限制已设为每人每局 {0} 次。", - "narrator.host_limit_zero":"旁白使用次数限制已设为 0(等效禁用)。", - "narrator.host_limit_usage":"用法:#narrator limit <次数>\n示例:#narrator limit 3", - "narrator.usage":"用法:#narrator <你要说的话>\n示例:#narrator 我被指控了但我有可视任务,怎么证明自己?" -} diff --git a/src/Empostor.Plugin.Narrator/NarratorAdminExtension.cs b/src/Empostor.Plugin.Narrator/NarratorAdminExtension.cs deleted file mode 100644 index b58ee03..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorAdminExtension.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Empostor.Api.Admin; -using Empostor.Api.Plugins; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorAdminExtension : IAdminExtension -{ - private readonly NarratorConfig _config; - private readonly string _configPath; - - public NarratorAdminExtension(NarratorConfig config, string configPath) - { - _config = config; - _configPath = configPath; - } - - public string Id => "narrator"; - - public string Title => "Narrator"; - - public string Icon => "chat"; - - public void Build(AdminPanelBuilder b) - { - b.RegisterTextbox(t => - { - t.Label = "DeepSeek API key"; - t.Value = _config.ApiKey; - t.Secret = true; - t.Placeholder = "sk-..."; - t.OnSubmit(async ctx => - { - _config.ApiKey = ctx.Value ?? string.Empty; - PluginConfigLoader.Save(_configPath, _config); - return AdminActionResult.Ok("API key saved."); - }); - }); - - b.RegisterTextbox(t => - { - t.Label = "Model"; - t.Value = _config.Model; - t.OnSubmit(async ctx => - { - _config.Model = ctx.Value ?? string.Empty; - PluginConfigLoader.Save(_configPath, _config); - return AdminActionResult.Ok("Model saved."); - }); - }); - - b.RegisterTextbox(t => - { - t.Label = "API endpoint"; - t.Value = _config.ApiEndpoint; - t.OnSubmit(async ctx => - { - _config.ApiEndpoint = ctx.Value ?? string.Empty; - PluginConfigLoader.Save(_configPath, _config); - return AdminActionResult.Ok("API endpoint saved."); - }); - }); - - b.RegisterNumber(n => - { - n.Label = "Max uses per game"; - n.Value = _config.MaxUsesPerGame; - n.Min = 0; - n.OnChange(async ctx => - { - if (int.TryParse(ctx.Value, out var v) && v >= 0) - { - _config.MaxUsesPerGame = v; - PluginConfigLoader.Save(_configPath, _config); - } - - return AdminActionResult.Ok("Max uses per game saved (applies to new games)."); - }); - }); - - b.RegisterNumber(n => - { - n.Label = "Max uses per meeting"; - n.Value = _config.MaxUsesPerMeeting; - n.Min = 0; - n.OnChange(async ctx => - { - if (int.TryParse(ctx.Value, out var v) && v >= 0) - { - _config.MaxUsesPerMeeting = v; - PluginConfigLoader.Save(_configPath, _config); - } - - return AdminActionResult.Ok("Max uses per meeting saved (applies to new games)."); - }); - }); - } -} diff --git a/src/Empostor.Plugin.Narrator/NarratorCommand.cs b/src/Empostor.Plugin.Narrator/NarratorCommand.cs deleted file mode 100644 index 5d8a183..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorCommand.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Text; -using System.Threading.Tasks; -using Empostor.Api.Commands; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorCommand : ICommand -{ - private readonly NarratorService _service; - private readonly ILogger _logger; - - public NarratorCommand(NarratorService service, ILogger logger) - { - _service = service; - _logger = logger; - } - - public string Name => "narrator"; - public string LocalizationOwner => NarratorPlugin.Owner; - - public string[] Aliases => new[] { "nar", "n" }; - - public string Description => "Ask the narrator for advice during a meeting."; - - public string Usage => "narrator "; - - public async ValueTask ExecuteAsync(CommandContext ctx) - { - var args = ctx.Args; - - // Host commands - if (args.Length > 0 && ctx.Sender.IsHost) - { - var sub = args[0].ToLowerInvariant(); - - if (sub == "enable" || sub == "on") - { - _service.SetEnabled(ctx.Game, true); - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.host_enabled", "Narrator enabled for this game."), ctx.PlayerControl); - return true; - } - - if (sub == "disable" || sub == "off") - { - _service.SetEnabled(ctx.Game, false); - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.host_disabled", "Narrator disabled for this game."), ctx.PlayerControl); - return true; - } - - if (sub == "status") - { - var enabled = _service.IsEnabled(ctx.Game); - var max = _service.GetMaxUses(ctx.Game); - var statusText = T(ctx, enabled ? "narrator.status_enabled" : "narrator.status_disabled", - enabled ? "enabled" : "disabled"); - var msg = T(ctx, "narrator.host_status", "Status: {0} | Max uses per player per game: {1}", statusText, max); - await ctx.PlayerControl.SendChatToPlayerAsync(msg, ctx.PlayerControl); - return true; - } - - if (sub == "limit") - { - if (args.Length < 2 || !int.TryParse(args[1], out var limit) || limit < 0) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.host_limit_usage", "Usage: #narrator limit "), ctx.PlayerControl); - return true; - } - - _service.SetMaxUses(ctx.Game, limit); - if (limit == 0) - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.host_limit_zero", "Limit set to 0 (effectively disabled)."), ctx.PlayerControl); - else - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.host_limit_set", "Limit set to {0} per player per game.", limit), - ctx.PlayerControl); - return true; - } - } - - // Show host commands if no args provided - if (args.Length == 0) - { - await ShowUsageAsync(ctx); - return true; - } - - // Check game state — meeting must be active - if (!_service.IsMeetingActive(ctx.Game)) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.meeting_only", "You can only use #narrator during a meeting."), ctx.PlayerControl); - return true; - } - - // Check if narrator is enabled - if (!_service.IsEnabled(ctx.Game)) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "narrator.disabled", "The narrator is currently disabled by the host."), ctx.PlayerControl); - return true; - } - - var playerName = ctx.PlayerControl.PlayerInfo.PlayerName; - - // Check usage limits - var (allowed, error) = _service.TryUse(ctx.Game, playerName); - if (!allowed) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - $"[Narrator] {error}", ctx.PlayerControl); - return true; - } - - var playerMessage = ctx.RawArgs?.Trim(); - if (string.IsNullOrEmpty(playerMessage)) - { - await ShowUsageAsync(ctx); - return true; - } - - _logger.LogInformation("[Narrator] {player} asks: {message}", playerName, playerMessage); - - var context = _service.BuildContext(ctx.Game, ctx.PlayerControl); - var reply = await _service.AskNarratorAsync(context, playerMessage); - - await ctx.PlayerControl.SendChatToPlayerAsync( - $"[Narrator] {reply}", ctx.PlayerControl); - - return true; - } - - private async ValueTask ShowUsageAsync(CommandContext ctx) - { - var sb = new StringBuilder(); - sb.AppendLine(T(ctx, "narrator.usage", - "Usage: #narrator ")); - if (ctx.Sender.IsHost) - sb.AppendLine(T(ctx, "narrator.host_commands", - "Host commands: #narrator enable | disable | limit ")); - await ctx.PlayerControl.SendChatToPlayerAsync(sb.ToString(), ctx.PlayerControl); - } - - private static string T(CommandContext ctx, string key, string defaultText, params object?[] arguments) - { - return ctx.GetPluginString(NarratorPlugin.Owner, key, arguments); - } -} diff --git a/src/Empostor.Plugin.Narrator/NarratorConfig.cs b/src/Empostor.Plugin.Narrator/NarratorConfig.cs deleted file mode 100644 index 3cf6f46..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorConfig -{ - [JsonPropertyName("apiKey")] - public string ApiKey { get; set; } = ""; - - [JsonPropertyName("model")] - public string Model { get; set; } = "deepseek-v4-flash"; - - [JsonPropertyName("apiEndpoint")] - public string ApiEndpoint { get; set; } = "https://api.deepseek.com/"; - - [JsonPropertyName("maxUsesPerGame")] - public int MaxUsesPerGame { get; set; } = 3; - - [JsonPropertyName("maxUsesPerMeeting")] - public int MaxUsesPerMeeting { get; set; } = 1; -} diff --git a/src/Empostor.Plugin.Narrator/NarratorEventListener.cs b/src/Empostor.Plugin.Narrator/NarratorEventListener.cs deleted file mode 100644 index 5293116..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorEventListener.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Empostor.Api.Events; -using Empostor.Api.Events.Player; -using Empostor.Api.Events.Meeting; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorEventListener : IEventListener -{ - private readonly NarratorService _service; - private readonly ILogger _logger; - - public NarratorEventListener(NarratorService service, ILogger logger) - { - _service = service; - _logger = logger; - } - - [EventListener] - public void OnGameStarted(IGameStartedEvent e) - { - _service.RecordGameStart(e.Game); - _logger.LogDebug("[Narrator] Game started: {code}", e.Game.Code); - } - - [EventListener] - public void OnGameEnded(IGameEndedEvent e) - { - _service.RecordGameEnd(e.Game); - _logger.LogDebug("[Narrator] Game ended: {code}", e.Game.Code); - } - - [EventListener] - public void OnPlayerCompletedTask(IPlayerCompletedTaskEvent e) - { - _service.RecordTask(e); - } - - [EventListener] - public void OnPlayerMurder(IPlayerMurderEvent e) - { - _service.RecordMurder(e); - } - - [EventListener] - public void OnPlayerExile(IPlayerExileEvent e) - { - _service.RecordExile(e); - } - - [EventListener] - public void OnPlayerReport(IPlayerReportEvent e) - { - _service.RecordReport(e); - } - - [EventListener] - public void OnMeetingStarted(IMeetingStartedEvent e) - { - _service.RecordMeetingStart(e); - _logger.LogDebug("[Narrator] Meeting started"); - } - - [EventListener] - public void OnMeetingEnded(IMeetingEndedEvent e) - { - _service.RecordMeetingEnd(e); - _logger.LogDebug("[Narrator] Meeting ended"); - } - - [EventListener] - public void OnPlayerChat(IPlayerChatEvent e) - { - _service.RecordChat(e); - } - - [EventListener] - public void OnPlayerVoted(IPlayerVotedEvent e) - { - _service.RecordVote(e); - } -} diff --git a/src/Empostor.Plugin.Narrator/NarratorPlugin.cs b/src/Empostor.Plugin.Narrator/NarratorPlugin.cs deleted file mode 100644 index dd56fd3..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorPlugin.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using Empostor.Api.Localization; -using Empostor.Api.Plugins; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugin.Narrator; - -[EmpostorPlugin(Owner)] -public sealed class NarratorPlugin : PluginBase -{ - public const string Owner = "cn.hayashiume.narrator"; - private readonly ILocalizationRegistry _registry; - private readonly ILogger _logger; - private IDisposable? _registration; - public NarratorPlugin(ILogger logger, ILocalizationRegistry registry) { _logger = logger; _registry = registry; } - public override ValueTask EnableAsync() - { - if (!_registry.TryRegister(new LocalizationCatalog(Owner, typeof(NarratorPlugin).Assembly, "Empostor.Plugin.Narrator.Localization"), out _registration)) - _logger.LogWarning("Could not register localization resources for {Owner}", Owner); - return default; - } - public override ValueTask DisableAsync() { _registration?.Dispose(); return default; } -} diff --git a/src/Empostor.Plugin.Narrator/NarratorService.cs b/src/Empostor.Plugin.Narrator/NarratorService.cs deleted file mode 100644 index bdf2b50..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorService.cs +++ /dev/null @@ -1,375 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Empostor.Api.Events.Player; -using Empostor.Api.Events.Meeting; -using Empostor.Api.Games; -using Empostor.Api.Innersloth.GameOptions; -using Empostor.Api.Net.Inner.Objects; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorService -{ - private readonly NarratorConfig _config; - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _games = new(); - private readonly ConcurrentDictionary _meetings = new(); - - public NarratorService(NarratorConfig config, ILogger logger) - { - _config = config; - _logger = logger; - _httpClient = new HttpClient(); - } - - // ---- Game lifecycle ---- - - public void RecordGameStart(IGame game) - { - _games[game.Code.ToString()] = new GameState - { - Enabled = true, - MaxUsesPerGame = _config.MaxUsesPerGame, - MaxUsesPerMeeting = _config.MaxUsesPerMeeting - }; - } - - public void RecordGameEnd(IGame game) - { - _games.TryRemove(game.Code.ToString(), out _); - _meetings.TryRemove(game.Code.ToString(), out _); - } - - // ---- Host controls ---- - - public bool IsEnabled(IGame game) - { - return _games.TryGetValue(game.Code.ToString(), out var state) && state.Enabled; - } - - public void SetEnabled(IGame game, bool enabled) - { - if (_games.TryGetValue(game.Code.ToString(), out var state)) - state.Enabled = enabled; - } - - public void SetMaxUses(IGame game, int max) - { - if (_games.TryGetValue(game.Code.ToString(), out var state)) - state.MaxUsesPerGame = Math.Max(0, max); - } - - public int GetMaxUses(IGame game) - { - return _games.TryGetValue(game.Code.ToString(), out var state) ? state.MaxUsesPerGame : _config.MaxUsesPerGame; - } - - // ---- Usage limits ---- - - public (bool allowed, string? error) TryUse(IGame game, string playerName) - { - var code = game.Code.ToString(); - if (!_games.TryGetValue(code, out var state)) - return (false, "Game state not found."); - - if (!state.Enabled) - return (false, "Narrator is disabled for this game by the host."); - - if (!IsMeetingActive(game)) - return (false, "You can only use this command during a meeting."); - - // Check per-meeting limit - var meetingUses = state.CurrentMeetingPlayerUses.GetValueOrDefault(playerName, 0); - if (meetingUses >= state.MaxUsesPerMeeting) - return (false, $"You've used the narrator {meetingUses} time(s) this meeting (limit: {state.MaxUsesPerMeeting})."); - - // Check per-game limit - var gameUses = state.PlayerUseCounts.GetValueOrDefault(playerName, 0); - if (gameUses >= state.MaxUsesPerGame) - return (false, $"You've used the narrator {gameUses} time(s) this game (limit: {state.MaxUsesPerGame})."); - - // Record usage - state.PlayerUseCounts[playerName] = gameUses + 1; - state.CurrentMeetingPlayerUses[playerName] = meetingUses + 1; - - return (true, null); - } - - // ---- Event recording ---- - - public void RecordTask(IPlayerCompletedTaskEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (!_games.TryGetValue(gameCode, out var state)) return; - - var playerName = e.PlayerControl.PlayerInfo.PlayerName; - if (!state.PlayerTasks.TryGetValue(playerName, out var tasks)) - { - tasks = new List(); - state.PlayerTasks[playerName] = tasks; - } - - var taskData = e.Task.Task; - tasks.Add(new TaskRecord - { - Name = taskData?.Name ?? $"Task#{e.Task.Id}", - IsVisual = taskData?.IsVisual ?? false - }); - } - - public void RecordMurder(IPlayerMurderEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (!_games.TryGetValue(gameCode, out var state)) return; - - var victimName = e.Victim.PlayerInfo.PlayerName; - state.DeadPlayers.Add(victimName); - - var killerName = e.PlayerControl.PlayerInfo.PlayerName; - state.Murders.Add(new MurderRecord { Killer = killerName, Victim = victimName }); - } - - public void RecordExile(IPlayerExileEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (!_games.TryGetValue(gameCode, out var state)) return; - - state.DeadPlayers.Add(e.PlayerControl.PlayerInfo.PlayerName); - } - - public void RecordReport(IPlayerReportEvent e) - { - _logger.LogDebug("[Narrator] {reporter} reported a body", e.PlayerControl.PlayerInfo.PlayerName); - } - - public void RecordMeetingStart(IMeetingStartedEvent e) - { - var gameCode = e.Game.Code.ToString(); - - // Reset per-meeting usage counters - if (_games.TryGetValue(gameCode, out var state)) - state.CurrentMeetingPlayerUses.Clear(); - - var meeting = new MeetingContext - { - IsActive = true, - Reporter = e.MeetingHud.Reporter?.PlayerName - }; - _meetings[gameCode] = meeting; - } - - public void RecordMeetingEnd(IMeetingEndedEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (_meetings.TryGetValue(gameCode, out var meeting)) - meeting.IsActive = false; - } - - public void RecordChat(IPlayerChatEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (!_meetings.TryGetValue(gameCode, out var meeting) || !meeting.IsActive) return; - - meeting.ChatMessages.Add(new ChatMessage - { - PlayerName = e.PlayerControl.PlayerInfo.PlayerName, - Message = e.Message - }); - } - - public void RecordVote(IPlayerVotedEvent e) - { - var gameCode = e.Game.Code.ToString(); - if (!_meetings.TryGetValue(gameCode, out var meeting) || !meeting.IsActive) return; - - var voterName = e.PlayerControl.PlayerInfo.PlayerName; - var votedFor = e.VotedFor?.PlayerInfo.PlayerName ?? "Skipped"; - meeting.Votes.Add(new VoteRecord { Voter = voterName, VotedFor = votedFor }); - } - - public bool IsMeetingActive(IGame game) - { - return _meetings.TryGetValue(game.Code.ToString(), out var meeting) && meeting.IsActive; - } - - // ---- Context & AI ---- - - public string BuildContext(IGame game, IInnerPlayerControl player) - { - var gameCode = game.Code.ToString(); - _games.TryGetValue(gameCode, out var state); - _meetings.TryGetValue(gameCode, out var meeting); - - var sb = new StringBuilder(); - var playerName = player.PlayerInfo.PlayerName; - - sb.AppendLine($"Your name: {playerName}"); - sb.AppendLine($"Your role: {(player.PlayerInfo.IsImpostor ? "Impostor" : "Crewmate")}"); - - if (game.Options is NormalGameOptions normalOpts) - { - sb.AppendLine($"Visual tasks enabled: {(normalOpts.VisualTasks ? "Yes" : "No")}"); - sb.AppendLine($"Confirm ejects: {(normalOpts.ConfirmImpostor ? "On" : "Off")}"); - } - - sb.AppendLine($"Total impostors: {game.Options.NumImpostors}"); - - if (state != null && state.PlayerTasks.TryGetValue(playerName, out var tasks) && tasks.Count > 0) - { - sb.AppendLine(); - sb.AppendLine("Your completed tasks:"); - foreach (var task in tasks) - { - var visualTag = task.IsVisual ? " [VISUAL TASK - others can see you do this!]" : ""; - sb.AppendLine($" - {task.Name}{visualTag}"); - } - } - - if (state != null && state.DeadPlayers.Count > 0) - { - sb.AppendLine(); - sb.AppendLine($"Dead players ({state.DeadPlayers.Count}):"); - foreach (var dead in state.DeadPlayers) - sb.AppendLine($" - {dead}"); - } - - if (meeting != null && meeting.IsActive) - { - sb.AppendLine(); - sb.AppendLine("--- Current Meeting ---"); - if (meeting.Reporter != null) - sb.AppendLine($"Meeting started by: {meeting.Reporter}"); - - if (meeting.ChatMessages.Count > 0) - { - sb.AppendLine("Chat log:"); - foreach (var msg in meeting.ChatMessages) - sb.AppendLine($" {msg.PlayerName}: {msg.Message}"); - } - - if (meeting.Votes.Count > 0) - { - sb.AppendLine("Votes so far:"); - foreach (var v in meeting.Votes) - sb.AppendLine($" {v.Voter} -> {v.VotedFor}"); - } - } - - return sb.ToString(); - } - - public async Task AskNarratorAsync(string context, string playerMessage) - { - if (string.IsNullOrEmpty(_config.ApiKey)) - return "Narrator is not configured. Please set your DeepSeek API key in the plugin config."; - - var systemPrompt = "You are a helpful narrator/assistant whispering advice to a player during an Among Us meeting. " - + "You help them formulate their defense and strategy. " - + "RULES: " - + "1. Only use information the player would know from their own perspective. " - + "2. If the player is Crewmate, help prove innocence — mention their visual tasks, alibis, logical arguments. " - + "3. If the player is Impostor, help them blend in with plausible lies consistent with known info. " - + "4. Be concise and natural, like a friend whispering advice. Keep it to 1-3 sentences. " - + "5. Never mention you are an AI or that this is a game. Just give the advice directly."; - - try - { - var requestBody = new - { - model = _config.Model, - messages = new[] - { - new { role = "system", content = systemPrompt }, - new { role = "user", content = $"Context:\n{context}\n\nThe player says: \"{playerMessage}\"\n\nGive them a short, helpful reply they can use in the meeting:" } - }, - max_tokens = 256, - temperature = 0.8 - }; - - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var request = new HttpRequestMessage(HttpMethod.Post, _config.ApiEndpoint) - { - Content = content - }; - request.Headers.Add("Authorization", $"Bearer {_config.ApiKey}"); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) - { - _logger.LogWarning("[Narrator] DeepSeek API error: {status} {body}", response.StatusCode, responseBody); - return "Sorry, the narrator is having trouble thinking right now. Try again in a moment."; - } - - using var doc = JsonDocument.Parse(responseBody); - var reply = doc.RootElement - .GetProperty("choices")[0] - .GetProperty("message") - .GetProperty("content") - .GetString(); - - return reply?.Trim() ?? "Sorry, I couldn't come up with a response."; - } - catch (Exception ex) - { - _logger.LogError(ex, "[Narrator] Failed to call DeepSeek API"); - return "Sorry, the narrator encountered an error. Please try again."; - } - } - - // ---- Private state classes ---- - - private sealed class GameState - { - public bool Enabled { get; set; } = true; - public int MaxUsesPerGame { get; set; } = 3; - public int MaxUsesPerMeeting { get; set; } = 1; - public Dictionary PlayerUseCounts { get; } = new(StringComparer.OrdinalIgnoreCase); - public Dictionary CurrentMeetingPlayerUses { get; } = new(StringComparer.OrdinalIgnoreCase); - public Dictionary> PlayerTasks { get; } = new(StringComparer.OrdinalIgnoreCase); - public HashSet DeadPlayers { get; } = new(StringComparer.OrdinalIgnoreCase); - public List Murders { get; } = new(); - } - - private sealed class MeetingContext - { - public bool IsActive { get; set; } - public string? Reporter { get; set; } - public List ChatMessages { get; } = new(); - public List Votes { get; } = new(); - } - - private sealed record TaskRecord - { - public string Name { get; set; } = ""; - public bool IsVisual { get; set; } - } - - private sealed record ChatMessage - { - public string PlayerName { get; set; } = ""; - public string Message { get; set; } = ""; - } - - private sealed record VoteRecord - { - public string Voter { get; set; } = ""; - public string VotedFor { get; set; } = ""; - } - - private sealed record MurderRecord - { - public string Killer { get; set; } = ""; - public string Victim { get; set; } = ""; - } -} diff --git a/src/Empostor.Plugin.Narrator/NarratorStartup.cs b/src/Empostor.Plugin.Narrator/NarratorStartup.cs deleted file mode 100644 index 79c6506..0000000 --- a/src/Empostor.Plugin.Narrator/NarratorStartup.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Empostor.Api.Admin; -using Empostor.Api.Commands; -using Empostor.Api.Events; -using Empostor.Api.Plugins; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Empostor.Plugin.Narrator; - -public sealed class NarratorStartup : IPluginStartup -{ - private string _configPath = "[Narrator]Config.json"; - - public void SetConfigPath(string configPath) => _configPath = configPath; - - public void ConfigureHost(IHostBuilder host) { } - - public void ConfigureServices(IServiceCollection services) - { - var config = PluginConfigLoader.Load(_configPath); - services.AddSingleton(config); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton( - sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton( - sp => sp.GetRequiredService()); - services.AddSingleton( - _ => new NarratorAdminExtension(config, _configPath)); - } -} diff --git a/src/Empostor.Plugin.Narrator/packages.lock.json b/src/Empostor.Plugin.Narrator/packages.lock.json deleted file mode 100644 index 5763b34..0000000 --- a/src/Empostor.Plugin.Narrator/packages.lock.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net8.0": { - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.1", - "contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA==" - }, - "Empostor.Hazel": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.1", - "Empostor.Hazel.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", - "Microsoft.Extensions.ObjectPool": "8.0.1", - "Serilog": "4.3.1" - } - }, - "Empostor.Hazel.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig==" - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.2.0", - "System.Text.Encodings.Web": "4.5.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "2.2.0" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.6", - "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g==" - }, - "empostor.api": { - "type": "Project", - "dependencies": { - "Empostor.Hazel": "[1.0.2, )", - "Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Empostor.Plugins.MapVote/Empostor.Plugins.MapVote.csproj b/src/Empostor.Plugins.MapVote/Empostor.Plugins.MapVote.csproj deleted file mode 100644 index 69f53fe..0000000 --- a/src/Empostor.Plugins.MapVote/Empostor.Plugins.MapVote.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0 - Empostor.Plugins.MapVote - enable - - - - - - - - diff --git a/src/Empostor.Plugins.MapVote/Localization/en.json b/src/Empostor.Plugins.MapVote/Localization/en.json deleted file mode 100644 index 4ab4325..0000000 --- a/src/Empostor.Plugins.MapVote/Localization/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "command.votemap.description":"Vote for the next map.", - "command.votemap.usage":"votemap ", - "mapvote.disabled":"Map voting is currently disabled by the host.", - "mapvote.unknown_map":"Unknown map: {0}. Available: Skeld, Mira, Polus, Airship, Fungle", - "mapvote.voted":"{0} voted for {1}.", - "mapvote.already_voted":"You already voted for {0}. Use #votemap to change your vote.", - "mapvote.no_votes":"No map votes yet. Use #votemap to vote!", - "mapvote.results_header":"=== Map Vote Results ===", - "mapvote.results_entry":" {0}: {1} vote(s)", - "mapvote.results_winner":"Winner: {0} ({1} votes)", - "mapvote.results_random":"No votes cast, using random map: {0}", - "mapvote.usage":"Usage: #votemap \nMaps: Skeld, Mira, Polus, Airship, Fungle\nExample: #votemap polus", - "mapvote.host_commands":"Host commands: #votemap public | close | enable | disable | results", - "mapvote.host_only":"Only the host can use this command.", - "mapvote.host_enabled":"Map voting enabled.", - "mapvote.host_disabled":"Map voting disabled.", - "mapvote.session_started":"Map vote session started! Players, use #votemap to vote.\nMaps: Skeld, Mira, Polus, Airship, Fungle", - "mapvote.map_set":"Map set to {0} by vote." -} diff --git a/src/Empostor.Plugins.MapVote/Localization/zh-CN.json b/src/Empostor.Plugins.MapVote/Localization/zh-CN.json deleted file mode 100644 index f2f15c7..0000000 --- a/src/Empostor.Plugins.MapVote/Localization/zh-CN.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "command.votemap.description":"投票选择下一张地图。", - "command.votemap.usage":"votemap <地图名>", - "mapvote.disabled":"投票选图功能已被房主禁用。", - "mapvote.unknown_map":"未知地图:{0}。可选:Skeld, Mira, Polus, Airship, Fungle", - "mapvote.voted":"{0} 投票选择了 {1}。", - "mapvote.already_voted":"你已经投票了 {0}。使用 #votemap <地图> 来修改投票。", - "mapvote.no_votes":"还没有人投票。使用 #votemap <地图> 来投票!", - "mapvote.results_header":"=== 地图投票结果 ===", - "mapvote.results_entry":" {0}:{1} 票", - "mapvote.results_winner":"胜出:{0}({1} 票)", - "mapvote.results_random":"无人投票,随机选择地图:{0}", - "mapvote.usage":"用法:#votemap <地图>\n可选地图:Skeld, Mira, Polus, Airship, Fungle\n示例:#votemap polus", - "mapvote.host_commands":"房主指令:#votemap public | close | enable | disable | results", - "mapvote.host_only":"只有房主可以使用此指令。", - "mapvote.host_enabled":"投票选图已启用。", - "mapvote.host_disabled":"投票选图已禁用。", - "mapvote.session_started":"地图投票已开始!请使用 #votemap <地图> 投票。\n可选地图:Skeld, Mira, Polus, Airship, Fungle", - "mapvote.map_set":"投票结果,地图已设为 {0}。" -} diff --git a/src/Empostor.Plugins.MapVote/MapVoteCommand.cs b/src/Empostor.Plugins.MapVote/MapVoteCommand.cs deleted file mode 100644 index 9fd27af..0000000 --- a/src/Empostor.Plugins.MapVote/MapVoteCommand.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Empostor.Api.Commands; - -namespace Empostor.Plugins.MapVote; - -public sealed class MapVoteCommand : ICommand -{ - private readonly MapVoteService _service; - private readonly MapVoteConfig _config; - - public MapVoteCommand(MapVoteService service, MapVoteConfig config) - { - _service = service; - _config = config; - } - - public string Name => "votemap"; - public string LocalizationOwner => MapVotePlugin.Owner; - public string[] Aliases => new[] { "vm" }; - public string Description => "Vote for the next map."; - public string Usage => "votemap "; - - public async ValueTask ExecuteAsync(CommandContext ctx) - { - var gameCode = ctx.Game.Code.ToString(); - - // Host control commands - if (ctx.Args.Length > 0) - { - var sub = ctx.Args[0].ToLowerInvariant(); - - if (sub == "public" || sub == "start") - { - if (!ctx.Sender.IsHost) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_only", "Only the host can start a map vote."), ctx.PlayerControl); - return true; - } - - _service.StartSession(gameCode); - - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.session_started", "Map vote session started! Players, use #votemap to vote.\nMaps: Skeld, Mira, Polus, Airship, Fungle"), ctx.PlayerControl); - return true; - } - - if (sub == "close" || sub == "end") - { - if (!ctx.Sender.IsHost) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_only", "Only the host can close the vote."), ctx.PlayerControl); - return true; - } - - var winner = _service.GetWinner(gameCode); - var tally = _service.TallyVotes(gameCode); - var voterCount = _service.VoterCount(gameCode); - - await ShowWinnerToAllAsync(ctx, winner, tally, voterCount); - - _service.StopSession(gameCode); - return true; - } - - if (sub == "enable" || sub == "on") - { - if (!ctx.Sender.IsHost) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_only", "Only the host can change settings."), ctx.PlayerControl); - return true; - } - - _service.SetEnabled(gameCode, true); - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_enabled", "Map voting enabled."), ctx.PlayerControl); - return true; - } - - if (sub == "disable" || sub == "off") - { - if (!ctx.Sender.IsHost) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_only", "Only the host can change settings."), ctx.PlayerControl); - return true; - } - - _service.SetEnabled(gameCode, false); - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.host_disabled", "Map voting disabled."), ctx.PlayerControl); - return true; - } - - if (sub == "results" || sub == "result") - { - await ShowResultsAsync(ctx); - return true; - } - } - - // Check if voting is enabled - if (!_service.IsEnabled(gameCode) && !ctx.Sender.IsHost) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.disabled", "Map voting is currently disabled by the host."), ctx.PlayerControl); - return true; - } - - // Voting requires a map argument - if (ctx.Args.Length == 0) - { - await ShowUsageAsync(ctx); - return true; - } - - if (!_service.TryParseMap(ctx.RawArgs!, out var map)) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.unknown_map", "Unknown map.", ctx.RawArgs ?? "?"), ctx.PlayerControl); - return true; - } - - var playerName = ctx.Sender.Character?.PlayerInfo?.PlayerName ?? ctx.Sender.Client.Name ?? "Unknown"; - _service.CastVote(gameCode, playerName, map); - - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.voted", "{0} voted for {1}.", playerName, MapVoteService.MapDisplayName(map)), - ctx.PlayerControl); - - return true; - } - - private async ValueTask ShowResultsAsync(CommandContext ctx) - { - var gameCode = ctx.Game.Code.ToString(); - var tally = _service.TallyVotes(gameCode); - - if (tally.Count == 0) - { - await ctx.PlayerControl.SendChatToPlayerAsync( - T(ctx, "mapvote.no_votes", "No map votes yet."), ctx.PlayerControl); - return; - } - - var sb = new StringBuilder(); - sb.AppendLine(T(ctx, "mapvote.results_header", "=== Map Vote Results ===")); - foreach (var (map, count) in tally) - { - sb.AppendLine(T(ctx, "mapvote.results_entry", " {0}: {1} vote(s)", MapVoteService.MapDisplayName(map), count)); - } - - await ctx.PlayerControl.SendChatToPlayerAsync(sb.ToString(), ctx.PlayerControl); - } - - private static async ValueTask ShowWinnerToAllAsync(CommandContext ctx, Api.Innersloth.MapTypes winner, - IReadOnlyDictionary tally, int voterCount) - { - foreach (var player in ctx.Game.Players) - { - var ctrl = player.Character; - if (ctrl != null) - { - var key = tally.Count == 0 ? "mapvote.results_random" : "mapvote.results_winner"; - var text = ctx.Localization.Get(new Empostor.Api.Localization.LocalizedMessageKey(MapVotePlugin.Owner, key), - player.Client.Language, MapVoteService.MapDisplayName(winner), tally.Count == 0 ? 0 : tally.Values.Max()); - await ctrl.SendChatToPlayerAsync(text, ctrl); - } - } - } - - private async ValueTask ShowUsageAsync(CommandContext ctx) - { - var sb = new StringBuilder(); - sb.AppendLine(T(ctx, "mapvote.usage", "Usage: #votemap \nMaps: Skeld, Mira, Polus, Airship, Fungle\nExample: #votemap polus")); - if (ctx.Sender.IsHost) - sb.AppendLine(T(ctx, "mapvote.host_commands", "Host commands: #votemap public | close | enable | disable | results")); - await ctx.PlayerControl.SendChatToPlayerAsync(sb.ToString(), ctx.PlayerControl); - } - - private static string T(CommandContext ctx, string key, string defaultText, params object?[] arguments) - { - return ctx.GetPluginString(MapVotePlugin.Owner, key, arguments); - } -} diff --git a/src/Empostor.Plugins.MapVote/MapVoteConfig.cs b/src/Empostor.Plugins.MapVote/MapVoteConfig.cs deleted file mode 100644 index 739e3ab..0000000 --- a/src/Empostor.Plugins.MapVote/MapVoteConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Empostor.Plugins.MapVote; - -public sealed class MapVoteConfig -{ - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } = true; - - [JsonPropertyName("require_majority")] - public bool RequireMajority { get; set; } = false; - - [JsonPropertyName("allow_host_override")] - public bool AllowHostOverride { get; set; } = true; -} diff --git a/src/Empostor.Plugins.MapVote/MapVoteEventListener.cs b/src/Empostor.Plugins.MapVote/MapVoteEventListener.cs deleted file mode 100644 index b809c3f..0000000 --- a/src/Empostor.Plugins.MapVote/MapVoteEventListener.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Threading.Tasks; -using Empostor.Api.Events; -using Empostor.Api.Events.Game; - -namespace Empostor.Plugins.MapVote; - -public sealed class MapVoteEventListener : IEventListener -{ - private readonly MapVoteService _service; - private readonly MapVoteConfig _config; - - public MapVoteEventListener(MapVoteService service, MapVoteConfig config) - { - _service = service; - _config = config; - } - - [EventListener] - public async ValueTask OnGameStarting(IGameStartingEvent e) - { - if (!_config.Enabled) return; - - var gameCode = e.Game.Code.ToString(); - - if (!_service.IsEnabled(gameCode)) return; - - var winner = _service.GetWinner(gameCode); - - // If host set a specific map and override is allowed, respect it when no votes cast - if (_config.AllowHostOverride && e.Game.Options.Map != winner) - { - var tally = _service.TallyVotes(gameCode); - if (tally.Count == 0) - { - _service.ResetVotes(gameCode); - return; - } - } - - e.Game.Options.Map = winner; - await e.Game.SyncSettingsAsync(); - - // Notify players - foreach (var player in e.Game.Players) - { - var ctrl = player.Character; - if (ctrl == null) continue; - await ctrl.SendChatToPlayerAsync( - $"Map set to {MapVoteService.MapDisplayName(winner)} by vote.", ctrl); - } - - _service.ResetVotes(gameCode); - } - - [EventListener] - public void OnGameDestroyed(IGameDestroyedEvent e) - { - _service.Remove(e.Game.Code.ToString()); - } -} diff --git a/src/Empostor.Plugins.MapVote/MapVotePlugin.cs b/src/Empostor.Plugins.MapVote/MapVotePlugin.cs deleted file mode 100644 index 1dd3cb2..0000000 --- a/src/Empostor.Plugins.MapVote/MapVotePlugin.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using Empostor.Api.Localization; -using Empostor.Api.Plugins; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.MapVote; - -[EmpostorPlugin(Owner)] -public sealed class MapVotePlugin : PluginBase -{ - public const string Owner = "cn.hayashiume.mapvote"; - private readonly ILocalizationRegistry _registry; - private readonly ILogger _logger; - private IDisposable? _registration; - public MapVotePlugin(ILogger logger, ILocalizationRegistry registry) { _logger = logger; _registry = registry; } - public override ValueTask EnableAsync() - { - if (!_registry.TryRegister(new LocalizationCatalog(Owner, typeof(MapVotePlugin).Assembly, "Empostor.Plugins.MapVote.Localization"), out _registration)) - _logger.LogWarning("Could not register localization resources for {Owner}", Owner); - return default; - } - public override ValueTask DisableAsync() { _registration?.Dispose(); return default; } -} diff --git a/src/Empostor.Plugins.MapVote/MapVoteService.cs b/src/Empostor.Plugins.MapVote/MapVoteService.cs deleted file mode 100644 index 2d4250a..0000000 --- a/src/Empostor.Plugins.MapVote/MapVoteService.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Empostor.Api.Innersloth; - -namespace Empostor.Plugins.MapVote; - -public sealed class MapVoteService -{ - private static readonly MapTypes[] AllMaps = - { - MapTypes.Skeld, MapTypes.MiraHQ, MapTypes.Polus, MapTypes.Airship, MapTypes.Fungle - }; - - private static readonly Dictionary MapAliases = new(StringComparer.OrdinalIgnoreCase) - { - ["skeld"] = MapTypes.Skeld, - ["the skeld"] = MapTypes.Skeld, - ["mira"] = MapTypes.MiraHQ, - ["mirahq"] = MapTypes.MiraHQ, - ["mira hq"] = MapTypes.MiraHQ, - ["polus"] = MapTypes.Polus, - ["airship"] = MapTypes.Airship, - ["fungle"] = MapTypes.Fungle, - // Chinese aliases - ["骷髅"] = MapTypes.Skeld, - ["米拉"] = MapTypes.MiraHQ, - ["波鲁斯"] = MapTypes.Polus, - ["飞船"] = MapTypes.Airship, - ["真菌"] = MapTypes.Fungle, - }; - - private readonly ConcurrentDictionary> _votes = new(); - private readonly ConcurrentDictionary _enabled = new(); - private readonly ConcurrentDictionary _sessionActive = new(); - - public bool IsEnabled(string gameCode) => _enabled.GetValueOrDefault(gameCode, true); - - public void SetEnabled(string gameCode, bool enabled) => _enabled[gameCode] = enabled; - - public bool IsSessionActive(string gameCode) => _sessionActive.GetValueOrDefault(gameCode, false); - - public void StartSession(string gameCode) => _sessionActive[gameCode] = true; - - public void StopSession(string gameCode) - { - _sessionActive.TryRemove(gameCode, out _); - _votes.TryRemove(gameCode, out _); - } - - public bool TryParseMap(string input, out MapTypes map) - { - if (MapAliases.TryGetValue(input.Trim(), out map)) - return true; - - if (int.TryParse(input.Trim(), out var id) && Enum.IsDefined(typeof(MapTypes), (byte)id)) - { - map = (MapTypes)(byte)id; - if (map != MapTypes.Dleks) - return true; - } - - return false; - } - - public static string MapDisplayName(MapTypes map) => map switch - { - MapTypes.Skeld => "The Skeld", - MapTypes.MiraHQ => "Mira HQ", - MapTypes.Polus => "Polus", - MapTypes.Airship => "Airship", - MapTypes.Fungle => "Fungle", - _ => map.ToString(), - }; - - public void CastVote(string gameCode, string playerName, MapTypes map) - { - var gameVotes = _votes.GetOrAdd(gameCode, _ => new ConcurrentDictionary()); - gameVotes[playerName] = map; - } - - public MapTypes? GetPlayerVote(string gameCode, string playerName) - { - if (_votes.TryGetValue(gameCode, out var gameVotes) && gameVotes.TryGetValue(playerName, out var map)) - return map; - return null; - } - - public IReadOnlyDictionary TallyVotes(string gameCode) - { - if (!_votes.TryGetValue(gameCode, out var gameVotes) || gameVotes.IsEmpty) - return new Dictionary(); - - return gameVotes.Values - .GroupBy(m => m) - .ToDictionary(g => g.Key, g => g.Count()); - } - - public int VoterCount(string gameCode) - { - if (!_votes.TryGetValue(gameCode, out var gameVotes)) - return 0; - return gameVotes.Count; - } - - public MapTypes GetWinner(string gameCode) - { - var tally = TallyVotes(gameCode); - if (tally.Count == 0) - { - var rng = new Random(); - return AllMaps[rng.Next(AllMaps.Length)]; - } - - var maxVotes = tally.Values.Max(); - var winners = tally.Where(kv => kv.Value == maxVotes).Select(kv => kv.Key).ToList(); - - if (winners.Count == 1) - return winners[0]; - - var rng2 = new Random(); - return winners[rng2.Next(winners.Count)]; - } - - public void ResetVotes(string gameCode) - { - _votes.TryRemove(gameCode, out _); - } - - internal void Remove(string gameCode) - { - _votes.TryRemove(gameCode, out _); - _enabled.TryRemove(gameCode, out _); - _sessionActive.TryRemove(gameCode, out _); - } -} diff --git a/src/Empostor.Plugins.MapVote/MapVoteStartup.cs b/src/Empostor.Plugins.MapVote/MapVoteStartup.cs deleted file mode 100644 index cfbc500..0000000 --- a/src/Empostor.Plugins.MapVote/MapVoteStartup.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Empostor.Api.Commands; -using Empostor.Api.Events; -using Empostor.Api.Plugins; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Empostor.Plugins.MapVote; - -public sealed class MapVoteStartup : IPluginStartup -{ - private string _configPath = "config"; - - public void SetConfigPath(string configPath) => _configPath = configPath; - - public void ConfigureHost(IHostBuilder host) { } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(_ => PluginConfigLoader.Load(_configPath)); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - } -} diff --git a/src/Empostor.Plugins.MapVote/packages.lock.json b/src/Empostor.Plugins.MapVote/packages.lock.json deleted file mode 100644 index 5763b34..0000000 --- a/src/Empostor.Plugins.MapVote/packages.lock.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net8.0": { - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.1", - "contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA==" - }, - "Empostor.Hazel": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.1", - "Empostor.Hazel.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", - "Microsoft.Extensions.ObjectPool": "8.0.1", - "Serilog": "4.3.1" - } - }, - "Empostor.Hazel.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig==" - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.2.0", - "System.Text.Encodings.Web": "4.5.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "2.2.0" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.6", - "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g==" - }, - "empostor.api": { - "type": "Project", - "dependencies": { - "Empostor.Hazel": "[1.0.2, )", - "Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Empostor.Plugins.PlayerLog/Empostor.Plugins.PlayerLog.csproj b/src/Empostor.Plugins.PlayerLog/Empostor.Plugins.PlayerLog.csproj deleted file mode 100644 index d2d1292..0000000 --- a/src/Empostor.Plugins.PlayerLog/Empostor.Plugins.PlayerLog.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0 - Empostor.Plugins.PlayerLog - enable - - - - - - - - diff --git a/src/Empostor.Plugins.PlayerLog/PlayerLogAdminExtension.cs b/src/Empostor.Plugins.PlayerLog/PlayerLogAdminExtension.cs deleted file mode 100644 index b8f5dfb..0000000 --- a/src/Empostor.Plugins.PlayerLog/PlayerLogAdminExtension.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Empostor.Api.Admin; - -namespace Empostor.Plugins.PlayerLog; - -/// -/// Admin panel page for the player log. The selected player, page and clear range live on this -/// singleton, so every rebuild of the panel reflects the last choice. -/// -public sealed class PlayerLogAdminExtension : IAdminExtension -{ - private const int PageSize = 100; - - private static readonly string[] TypeColors = { "Chat", "Report", "Murder", "Exile", "Vote", "Task", "Vent", "Meeting" }; - - private readonly PlayerLogStore _store; - - private int? _clientId; - private int _page = 1; - private string _clearRange = "all"; - - public PlayerLogAdminExtension(PlayerLogStore store) - { - _store = store; - } - - public string Id => "player-log"; - - public string Title => "Player Log"; - - public string Icon => "list"; - - public string Section => "Server"; - - /// The chat feed mirrors live server state, so keep the 1-second rebuild. - public bool AutoRefresh => true; - - public void Build(AdminPanelBuilder b) - { - var players = _store.GetLoggedClientIds(); - if (_clientId.HasValue && !players.Contains(_clientId.Value)) - { - _clientId = null; - } - - var total = _clientId.HasValue ? _store.GetCountByClient(_clientId.Value) : _store.GetCount(); - var totalPages = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize)); - _page = Math.Clamp(_page, 1, totalPages); - - var entries = _clientId.HasValue - ? _store.GetPageByClient(_clientId.Value, _page, PageSize) - : _store.GetPage(_page, PageSize); - - b.RegisterSelect(sel => - { - sel.Label = "Player"; - sel.Options = new List { new("All players", string.Empty) }; - foreach (var id in players) - { - var name = _store.GetLatestName(id) ?? $"Player#{id}"; - var fc = _store.GetLatestFriendCode(id) ?? "—"; - sel.Options.Add(new AdminOption($"#{id} {name} ({fc})", id.ToString())); - } - - sel.Value = _clientId?.ToString() ?? string.Empty; - sel.OnChange(ctx => - { - _clientId = int.TryParse(ctx.Value, out var id) ? id : null; - _page = 1; - return ValueTask.FromResult(AdminActionResult.Ok(null)); - }); - }); - - b.RegisterSelect(sel => - { - sel.Label = "Page"; - sel.Options = Enumerable.Range(1, totalPages) - .Select(p => new AdminOption($"{p} / {totalPages}", p.ToString())) - .ToList(); - sel.Value = _page.ToString(); - sel.OnChange(ctx => - { - if (int.TryParse(ctx.Value, out var page)) - { - _page = page; - } - - return ValueTask.FromResult(AdminActionResult.Ok(null)); - }); - }); - - b.RegisterTable(t => - { - t.Columns = new List { "Time", "Type", "Player", "Friend Code", "Game", "Detail" }; - foreach (var entry in entries) - { - t.Rows.Add(new List - { - new() { Text = entry.Time.ToLocalTime().ToString("MM-dd HH:mm:ss") }, - new() { Text = entry.Type, Tone = TypeColors.Contains(entry.Type) ? "accent" : "default" }, - new() { Text = entry.PlayerName ?? "—" }, - new() { Text = entry.FriendCode ?? "—", Monospace = true }, - new() { Text = entry.GameCode ?? "—", Monospace = true }, - new() { Text = entry.Detail ?? "—" }, - }); - } - }); - - b.RegisterText(txt => - { - txt.Content = total == 0 - ? "No log entries recorded yet." - : $"Showing {entries.Count} of {total} entr{(total == 1 ? "y" : "ies")} · export: /player-log/export"; - txt.Tone = "muted"; - }); - - b.RegisterButton(btn => - { - btn.Label = "Export as JSON"; - btn.Icon = "download"; - btn.Style = "secondary"; - btn.OnClick(ctx => - { - var bytes = _clientId.HasValue ? _store.ExportJson(_clientId.Value) : _store.ExportJson(); - var scope = _clientId.HasValue ? $"client {_clientId}" : "all players"; - return ValueTask.FromResult(AdminActionResult.Ok( - $"{bytes.Length} bytes ready for {scope}. Open /player-log/export to download.")); - }); - }); - - b.RegisterSelect(sel => - { - sel.Label = "Clear logs"; - sel.Options = new List - { - new("All logs", "all"), - new("Older than 1 hour", "1h"), - new("Older than 24 hours", "24h"), - new("Older than 7 days", "7d"), - new("Older than 30 days", "30d"), - }; - sel.Value = _clearRange; - sel.OnChange(ctx => - { - _clearRange = string.IsNullOrWhiteSpace(ctx.Value) ? "all" : ctx.Value!; - return ValueTask.FromResult(AdminActionResult.Ok(null)); - }); - }); - - b.RegisterButton(btn => - { - btn.Label = "Clear Logs"; - btn.Icon = "trash"; - btn.Style = "danger"; - btn.OnClick(ctx => - { - var cutoff = _clearRange switch - { - "1h" => DateTime.UtcNow.AddHours(-1), - "24h" => DateTime.UtcNow.AddDays(-1), - "7d" => DateTime.UtcNow.AddDays(-7), - "30d" => DateTime.UtcNow.AddDays(-30), - _ => (DateTime?)null, - }; - - _store.Clear(cutoff); - _page = 1; - var scope = cutoff.HasValue ? $"older than {_clearRange}" : "all"; - return ValueTask.FromResult(AdminActionResult.Ok($"Cleared {scope} log entries.")); - }); - }); - } -} diff --git a/src/Empostor.Plugins.PlayerLog/PlayerLogListener.cs b/src/Empostor.Plugins.PlayerLog/PlayerLogListener.cs deleted file mode 100644 index 984ce43..0000000 --- a/src/Empostor.Plugins.PlayerLog/PlayerLogListener.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Empostor.Api.Events; -using Empostor.Api.Events.Client; -using Empostor.Api.Events.Meeting; -using Empostor.Api.Events.Player; -using Empostor.Api.Innersloth; - -namespace Empostor.Plugins.PlayerLog; - -internal sealed class PlayerLogListener : IEventListener -{ - private readonly PlayerLogStore _store; - - public PlayerLogListener(PlayerLogStore store) => _store = store; - - private static string? Gc(IGameEvent e) => e.Game != null ? GameCodeParser.IntToGameName(e.Game.Code) : null; - - private static string? Pn(IPlayerEvent e) => e.ClientPlayer?.Client?.Name; - - private static string? Pf(IPlayerEvent e) => e.ClientPlayer?.Client?.FriendCode; - - private static int? Pid(IPlayerEvent e) => e.ClientPlayer?.Client?.Id; - - // Chat - [EventListener] - public void OnChat(IPlayerChatEvent e) => - _store.Add("Chat", Pid(e), Pn(e), Pf(e), Gc(e), e.Message); - - // RPC-like events - [EventListener] - public void OnReport(IPlayerReportEvent e) => - _store.Add("Report", Pid(e), Pn(e), Pf(e), Gc(e), $"Reported: {e.ReportedClient?.Name ?? "body"}"); - - [EventListener] - public void OnMurder(IPlayerMurderEvent e) => - _store.Add("Murder", Pid(e), Pn(e), Pf(e), Gc(e), $"Victim: {e.Victim?.PlayerInfo?.PlayerName ?? "unknown"}"); - - [EventListener] - public void OnExile(IPlayerExileEvent e) => - _store.Add("Exile", Pid(e), Pn(e), Pf(e), Gc(e), "Ejected"); - - [EventListener] - public void OnVote(IPlayerVotedEvent e) => - _store.Add("Vote", Pid(e), Pn(e), Pf(e), Gc(e), $"Voted: {e.VotedFor?.PlayerInfo?.PlayerName ?? "skipped"}"); - - [EventListener] - public void OnTask(IPlayerCompletedTaskEvent e) => - _store.Add("Task", Pid(e), Pn(e), Pf(e), Gc(e), null); - - [EventListener] - public void OnEnterVent(IPlayerEnterVentEvent e) => - _store.Add("Vent", Pid(e), Pn(e), Pf(e), Gc(e), "Enter vent"); - - [EventListener] - public void OnExitVent(IPlayerExitVentEvent e) => - _store.Add("Vent", Pid(e), Pn(e), Pf(e), Gc(e), "Exit vent"); - - // Meeting events (game-level, not player-level) - [EventListener] - public void OnMeetingStarted(IMeetingStartedEvent e) => - _store.Add("Meeting", null, null, null, Gc(e), "Meeting started"); - - [EventListener] - public void OnMeetingEnded(IMeetingEndedEvent e) => - _store.Add("Meeting", null, null, null, Gc(e), "Meeting ended"); - - // Connection events - [EventListener] - public void OnClientConnected(IClientConnectedEvent e) => - _store.Add("Connect", e.Client.Id, e.Client.Name, e.Client.FriendCode, null, null); - - // Game lifecycle - [EventListener] - public void OnGameCreated(IGameCreatedEvent e) => - _store.Add("Game", null, null, null, Gc(e), "Game created"); - - [EventListener] - public void OnGameStarted(IGameStartedEvent e) => - _store.Add("Game", null, null, null, Gc(e), "Game started"); - - [EventListener] - public void OnGameEnded(IGameEndedEvent e) => - _store.Add("Game", null, null, null, Gc(e), $"Game ended: {e.GameOverReason}"); - - [EventListener] - public void OnGameDestroyed(IGameDestroyedEvent e) => - _store.Add("Game", null, null, null, Gc(e), "Game destroyed"); - - // Player join/leave - [EventListener] - public void OnPlayerJoined(IGamePlayerJoinedEvent e) => - _store.Add("Join", e.Player.Client.Id, e.Player.Client.Name, e.Player.Client.FriendCode, Gc(e), null); - - [EventListener] - public void OnPlayerLeft(IGamePlayerLeftEvent e) => - _store.Add("Leave", e.Player.Client.Id, e.Player.Client.Name, e.Player.Client.FriendCode, Gc(e), e.IsBan ? "Banned" : "Left"); -} diff --git a/src/Empostor.Plugins.PlayerLog/PlayerLogPlugin.cs b/src/Empostor.Plugins.PlayerLog/PlayerLogPlugin.cs deleted file mode 100644 index 12fa59a..0000000 --- a/src/Empostor.Plugins.PlayerLog/PlayerLogPlugin.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Empostor.Api.Plugins; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.PlayerLog; - -[EmpostorPlugin("gg.empostor.playerlog")] -public sealed class PlayerLogPlugin : PluginBase -{ - private readonly ILogger _logger; - - public PlayerLogPlugin(ILogger logger) - { - _logger = logger; - } - - public override ValueTask EnableAsync() - { - _logger.LogInformation("[PlayerLog] Enabled. Player activity logs are available in the admin panel."); - return default; - } - - public override ValueTask DisableAsync() => default; -} diff --git a/src/Empostor.Plugins.PlayerLog/PlayerLogPluginStartup.cs b/src/Empostor.Plugins.PlayerLog/PlayerLogPluginStartup.cs deleted file mode 100644 index 43159b2..0000000 --- a/src/Empostor.Plugins.PlayerLog/PlayerLogPluginStartup.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using Empostor.Api.Admin; -using Empostor.Api.Config; -using Empostor.Api.Events; -using Empostor.Api.Plugins; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; - -namespace Empostor.Plugins.PlayerLog; - -public sealed class PlayerLogPluginStartup : IPluginStartup, IPluginHttpStartup -{ - public void ConfigureHost(IHostBuilder host) { } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - public void ConfigureWebApplication(IApplicationBuilder app) - { - app.Map("/player-log/export", branch => branch.Run(async context => - { - if (!IsAdmin(context)) - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; - } - - var store = context.RequestServices.GetRequiredService(); - var raw = context.Request.Query["clientId"].ToString(); - var bytes = int.TryParse(raw, out var clientId) ? store.ExportJson(clientId) : store.ExportJson(); - - context.Response.ContentType = "application/json"; - context.Response.Headers["Content-Disposition"] = "attachment; filename=player_logs.json"; - await context.Response.Body.WriteAsync(bytes); - })); - } - - private static bool IsAdmin(HttpContext context) - { - var config = context.RequestServices.GetRequiredService>().Value; - if (string.IsNullOrEmpty(config.Password)) - { - return false; - } - - return context.Request.Cookies.TryGetValue("empostor_admin", out var value) - && string.Equals(value, Hash(config.Password), StringComparison.Ordinal); - } - - private static string Hash(string input) - => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); -} diff --git a/src/Empostor.Plugins.PlayerLog/PlayerLogStore.cs b/src/Empostor.Plugins.PlayerLog/PlayerLogStore.cs deleted file mode 100644 index 7be314d..0000000 --- a/src/Empostor.Plugins.PlayerLog/PlayerLogStore.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading; -using Empostor.Api.Service; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.PlayerLog; - -public sealed class PlayerLogEntry -{ - public DateTime Time { get; init; } - - public string Type { get; init; } = string.Empty; - - public int? ClientId { get; init; } - - public string? PlayerName { get; init; } - - public string? FriendCode { get; init; } - - public string? GameCode { get; init; } - - public string? Detail { get; init; } -} - -public sealed class PlayerLogStore : JsonDataStore> -{ - private const int MaxEntries = 10000; - private readonly ConcurrentQueue _entries = new(); - private int _count; - - public PlayerLogStore(ILogger logger) - : base(logger, legacyPath: "Data/player_logs.json") - { - Load(); - } - - public void Add(string type, int? clientId, string? playerName, string? friendCode, string? gameCode, string? detail) - { - var entry = new PlayerLogEntry - { - Time = DateTime.UtcNow, - Type = type, - ClientId = clientId, - PlayerName = playerName, - FriendCode = friendCode, - GameCode = gameCode, - Detail = detail, - }; - - _entries.Enqueue(entry); - - if (Interlocked.Increment(ref _count) > MaxEntries) - { - _entries.TryDequeue(out _); - Interlocked.Decrement(ref _count); - } - - SaveFireAndForget(); - } - - public List GetAll() => _entries.ToList(); - - public List GetByClient(int clientId) => - _entries.Where(e => e.ClientId == clientId).ToList(); - - public int GetCount() => _count; - - public int GetCountByClient(int clientId) => - _entries.Count(e => e.ClientId == clientId); - - public List GetPage(int page, int pageSize) - { - var skip = (page - 1) * pageSize; - return _entries.Skip(skip).Take(pageSize).ToList(); - } - - public List GetPageByClient(int clientId, int page, int pageSize) - { - var skip = (page - 1) * pageSize; - return _entries.Where(e => e.ClientId == clientId).Skip(skip).Take(pageSize).ToList(); - } - - public List GetLoggedClientIds() => - _entries.Where(e => e.ClientId.HasValue).Select(e => e.ClientId!.Value).Distinct().ToList(); - - public int GetMaxClientId() => - _entries.Where(e => e.ClientId.HasValue) - .Select(e => e.ClientId!.Value) - .DefaultIfEmpty(0) - .Max(); - - public string? GetLatestName(int clientId) => - _entries.Where(e => e.ClientId == clientId) - .OrderByDescending(e => e.Time) - .Select(e => e.PlayerName) - .FirstOrDefault(n => !string.IsNullOrEmpty(n)); - - public string? GetLatestFriendCode(int clientId) => - _entries.Where(e => e.ClientId == clientId) - .OrderByDescending(e => e.Time) - .Select(e => e.FriendCode) - .FirstOrDefault(f => !string.IsNullOrEmpty(f)); - - public void Clear(DateTime? olderThan = null) - { - if (olderThan.HasValue) - { - var kept = _entries.Where(e => e.Time >= olderThan.Value).ToList(); - _entries.Clear(); - _count = 0; - foreach (var entry in kept) - { - _entries.Enqueue(entry); - Interlocked.Increment(ref _count); - } - } - else - { - _entries.Clear(); - _count = 0; - } - - SaveFireAndForget(); - } - - public byte[] ExportJson() - { - var json = JsonSerializer.Serialize(_entries.ToList(), JsonOpts); - return Encoding.UTF8.GetBytes(json); - } - - public byte[] ExportJson(int clientId) - { - var list = _entries.Where(e => e.ClientId == clientId).ToList(); - var json = JsonSerializer.Serialize(list, JsonOpts); - return Encoding.UTF8.GetBytes(json); - } - - protected override List GetSnapshot() => _entries.ToList(); - - protected override void ApplySnapshot(List data) - { - _entries.Clear(); - _count = 0; - foreach (var entry in data) - { - _entries.Enqueue(entry); - Interlocked.Increment(ref _count); - } - - while (_count > MaxEntries) - { - _entries.TryDequeue(out _); - Interlocked.Decrement(ref _count); - } - } -} diff --git a/src/Empostor.Plugins.PlayerLog/packages.lock.json b/src/Empostor.Plugins.PlayerLog/packages.lock.json deleted file mode 100644 index 5763b34..0000000 --- a/src/Empostor.Plugins.PlayerLog/packages.lock.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net8.0": { - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.1", - "contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA==" - }, - "Empostor.Hazel": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.1", - "Empostor.Hazel.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", - "Microsoft.Extensions.ObjectPool": "8.0.1", - "Serilog": "4.3.1" - } - }, - "Empostor.Hazel.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig==" - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.2.0", - "System.Text.Encodings.Web": "4.5.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "2.2.0" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.6", - "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g==" - }, - "empostor.api": { - "type": "Project", - "dependencies": { - "Empostor.Hazel": "[1.0.2, )", - "Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Empostor.Plugins.Titles/Empostor.Plugins.Titles.csproj b/src/Empostor.Plugins.Titles/Empostor.Plugins.Titles.csproj deleted file mode 100644 index d553281..0000000 --- a/src/Empostor.Plugins.Titles/Empostor.Plugins.Titles.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0 - Empostor.Plugins.Titles - enable - - - - - - - - diff --git a/src/Empostor.Plugins.Titles/Localization/en.json b/src/Empostor.Plugins.Titles/Localization/en.json deleted file mode 100644 index 4b10947..0000000 --- a/src/Empostor.Plugins.Titles/Localization/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "titles.display_format": "[{0}] {1}", - "command.title.too_long": "Title too long. Max 12 characters.", - "command.title.cleared": "Title removed.", - "command.title.set": "Title set: {0}" -} diff --git a/src/Empostor.Plugins.Titles/Localization/zh-CN.json b/src/Empostor.Plugins.Titles/Localization/zh-CN.json deleted file mode 100644 index fa722fa..0000000 --- a/src/Empostor.Plugins.Titles/Localization/zh-CN.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "titles.display_format": "[{0}] {1}", - "command.title.too_long": "头衔过长,最多 12 个字符。", - "command.title.cleared": "头衔已移除。", - "command.title.set": "头衔已设置:{0}" -} diff --git a/src/Empostor.Plugins.Titles/Service/FriendCodeTitleListener.cs b/src/Empostor.Plugins.Titles/Service/FriendCodeTitleListener.cs deleted file mode 100644 index 40fc2ae..0000000 --- a/src/Empostor.Plugins.Titles/Service/FriendCodeTitleListener.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Empostor.Api.Events; -using Empostor.Api.Events.Client; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.Titles.Service; - -public sealed class FriendCodeTitleListener : IEventListener -{ - private readonly ILogger _logger; - private readonly TitlesConfig _config; - private readonly TitleStore _store; - private Dictionary _map; - - public FriendCodeTitleListener( - ILogger logger, - TitlesConfig config, - TitleStore store) - { - _logger = logger; - _config = config; - _store = store; - _map = BuildMap(); - - _logger.LogInformation("[FriendCodeTitles] Loaded {Count} title mapping(s).", _map.Count); - } - - /// - /// Reloads the title mappings from config. Called by the API middleware - /// after a new title is added. - /// - public void Reload() - { - _map = BuildMap(); - _logger.LogInformation("[FriendCodeTitles] Reloaded {Count} title mapping(s).", _map.Count); - } - - [EventListener] - public void OnClientConnected(IClientConnectedEvent e) - { - var fc = e.Client.FriendCode; - if (string.IsNullOrEmpty(fc)) return; - if (!_map.TryGetValue(fc, out var title)) return; - - try - { - // Store title so TitleEventListener can apply it in-game via SetNameAsync - _store.Set(e.Client.Id, title); - - _logger.LogInformation("[FriendCodeTitles] Stored [{Title}] for {Name} ({FC})", - title, e.Client.Name, fc); - - // Remove applied title so it is only used once - _map.Remove(fc); - - // Also remove from config so it persists across reloads - var entry = _config.Titles.FirstOrDefault(t => - string.Equals(t.FriendCode, fc, StringComparison.OrdinalIgnoreCase)); - if (entry != null) - { - _config.Titles.Remove(entry); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "[FriendCodeTitles] Failed to apply title for {FC}", fc); - } - } - - private Dictionary BuildMap() - { - return _config.Titles - .Where(t => !string.IsNullOrWhiteSpace(t.FriendCode) && !string.IsNullOrWhiteSpace(t.Title)) - .ToDictionary(t => t.FriendCode, t => t.Title, StringComparer.OrdinalIgnoreCase); - } -} diff --git a/src/Empostor.Plugins.Titles/Service/TitleApiMiddleware.cs b/src/Empostor.Plugins.Titles/Service/TitleApiMiddleware.cs deleted file mode 100644 index 8bc1f12..0000000 --- a/src/Empostor.Plugins.Titles/Service/TitleApiMiddleware.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.Titles.Service; - -public sealed class TitleApiMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly TitlesConfig _config; - - private static readonly string ConfigPath = Path.Combine( - Path.GetDirectoryName(typeof(TitleApiMiddleware).Assembly.Location) ?? ".", - "[Title System]Config.json"); - - public TitleApiMiddleware( - RequestDelegate next, - ILogger logger, - TitlesConfig config) - { - _next = next; - _logger = logger; - _config = config; - } - - public async Task InvokeAsync(HttpContext context) - { - if (context.Request.Path == "/api/title/add" - && context.Request.Method == "POST") - { - await HandleAddTitleAsync(context); - return; - } - - await _next(context); - } - - private async Task HandleAddTitleAsync(HttpContext context) - { - try - { - using var reader = new StreamReader(context.Request.Body); - var body = await reader.ReadToEndAsync(); - - var request = JsonSerializer.Deserialize(body); - if (request == null - || string.IsNullOrWhiteSpace(request.FriendCode) - || string.IsNullOrWhiteSpace(request.Title)) - { - context.Response.StatusCode = 400; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync( - "{\"ok\":false,\"error\":\"friendCode and title are required.\"}"); - return; - } - - // Add to config - _config.Titles.Add(new FriendCodeTitle - { - FriendCode = request.FriendCode.Trim(), - Title = request.Title.Trim() - }); - - // Persist to file - var json = JsonSerializer.Serialize(_config, new JsonSerializerOptions - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); - await File.WriteAllTextAsync(ConfigPath, json); - - // Reload mappings in the listener - var listener = context.RequestServices.GetService(); - listener?.Reload(); - - var addedBy = request.AddedBy ?? "unknown"; - _logger.LogInformation( - "[TitleApi] Added title [{Title}] for friend code {FC} (added by {AddedBy})", - request.Title, request.FriendCode, addedBy); - - context.Response.StatusCode = 200; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync("{\"ok\":true}"); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "[TitleApi] Failed to add title"); - context.Response.StatusCode = 500; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync( - "{\"ok\":false,\"error\":\"Internal server error.\"}"); - } - } - - private sealed record TitleAddRequest( - [property: JsonPropertyName("friendCode")] - string? FriendCode, - [property: JsonPropertyName("title")] - string? Title, - [property: JsonPropertyName("addedBy")] - string? AddedBy); -} diff --git a/src/Empostor.Plugins.Titles/Service/TitleEventListener.cs b/src/Empostor.Plugins.Titles/Service/TitleEventListener.cs deleted file mode 100644 index 29144c6..0000000 --- a/src/Empostor.Plugins.Titles/Service/TitleEventListener.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading.Tasks; -using Empostor.Api.Events; -using Empostor.Api.Events.Player; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.Titles.Service; - -public sealed class TitleEventListener : IEventListener -{ - private readonly ILogger _logger; - private readonly TitleStore _store; - - public TitleEventListener(ILogger logger, TitleStore store) - { - _logger = logger; - _store = store; - } - - [EventListener] - public async ValueTask OnPlayerSpawned(IPlayerSpawnedEvent e) - { - var clientId = e.ClientPlayer.Client.Id; - var title = _store.Get(clientId); - if (title == null) return; - - // Guard: connection must still be alive - if (e.ClientPlayer.Client.Connection == null - || !e.ClientPlayer.Client.Connection.IsConnected) - return; - - try - { - var displayName = TitleStore.BuildDisplayName(title, e.ClientPlayer.Client.Name); - await e.PlayerControl.SetNameAsync(displayName); - - // Title applied in-game, clear from store - _store.Clear(clientId); - - _logger.LogDebug("[Titles] In-game name set [{Title}] for client {Id}", - title, clientId); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "[Titles] Failed to apply title for client {Id}", clientId); - } - } - - [EventListener] - public void OnPlayerDestroyed(IPlayerDestroyedEvent e) - { - // Clean up any lingering title - //_store.Clear(e.ClientPlayer.Client.Id); - } -} diff --git a/src/Empostor.Plugins.Titles/TitleStore.cs b/src/Empostor.Plugins.Titles/TitleStore.cs deleted file mode 100644 index 1d72b4a..0000000 --- a/src/Empostor.Plugins.Titles/TitleStore.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Concurrent; - -namespace Empostor.Plugins.Titles; - -public sealed class TitleStore -{ - private readonly ConcurrentDictionary _titles = new(); - - public void Set(int clientId, string? title) - { - if (string.IsNullOrWhiteSpace(title)) - { - _titles.TryRemove(clientId, out _); - } - else - { - _titles[clientId] = title.Trim(); - } - } - - public string? Get(int clientId) - => _titles.TryGetValue(clientId, out var t) ? t : null; - - public void Clear(int clientId) => _titles.TryRemove(clientId, out _); - - public static string BuildDisplayName(string title, string playerName) - => $"[{title}] {playerName}"; -} diff --git a/src/Empostor.Plugins.Titles/TitlesAdminExtension.cs b/src/Empostor.Plugins.Titles/TitlesAdminExtension.cs deleted file mode 100644 index 81e475e..0000000 --- a/src/Empostor.Plugins.Titles/TitlesAdminExtension.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Empostor.Api.Admin; -using Empostor.Api.Plugins; -using Empostor.Plugins.Titles.Service; - -namespace Empostor.Plugins.Titles; - -public sealed class TitlesAdminExtension : IAdminExtension -{ - private readonly TitlesConfig _config; - private readonly string _configPath; - private readonly FriendCodeTitleListener _listener; - - public TitlesAdminExtension(TitlesConfig config, string configPath, FriendCodeTitleListener listener) - { - _config = config; - _configPath = configPath; - _listener = listener; - } - - public string Id => "titles"; - - public string Title => "Title System"; - - public string Icon => "list"; - - public void Build(AdminPanelBuilder b) - { - b.RegisterTable(table => - { - table.Columns = new List { "Friend Code", "Title" }; - table.Rows = _config.Titles.Select(t => new List - { - new() { Text = t.FriendCode, Monospace = true }, - new() { Text = t.Title }, - }).ToList(); - }); - - b.RegisterTextbox(t => - { - t.Label = "Edit titles (JSON)"; - t.Multiline = true; - t.Value = JsonSerializer.Serialize(_config.Titles, new JsonSerializerOptions { WriteIndented = true }); - t.OnSubmit(async ctx => - { - try - { - var list = JsonSerializer.Deserialize>(ctx.Value ?? "[]"); - if (list == null) - { - return AdminActionResult.Fail("Invalid JSON."); - } - - _config.Titles = list; - PluginConfigLoader.Save(_configPath, _config); - _listener.Reload(); - return AdminActionResult.Ok($"Saved {list.Count} title(s)."); - } - catch (JsonException ex) - { - return AdminActionResult.Fail("Invalid JSON: " + ex.Message); - } - }); - }); - } -} diff --git a/src/Empostor.Plugins.Titles/TitlesConfig.cs b/src/Empostor.Plugins.Titles/TitlesConfig.cs deleted file mode 100644 index 86b1338..0000000 --- a/src/Empostor.Plugins.Titles/TitlesConfig.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Empostor.Plugins.Titles; - -public sealed class TitlesConfig -{ - [JsonPropertyName("titles")] - public List Titles { get; set; } = new() - { - new FriendCodeTitle { FriendCode = "aideproof#8388", Title = "Empostor" }, - }; -} - -public sealed class FriendCodeTitle -{ - [JsonPropertyName("friendCode")] - public string FriendCode { get; set; } = string.Empty; - - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; -} diff --git a/src/Empostor.Plugins.Titles/TitlesPlugin.cs b/src/Empostor.Plugins.Titles/TitlesPlugin.cs deleted file mode 100644 index 3d60692..0000000 --- a/src/Empostor.Plugins.Titles/TitlesPlugin.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using Empostor.Api.Localization; -using Empostor.Api.Plugins; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.Titles; - -[EmpostorPlugin(Owner, "Title System", "Empostor", "1.1.0")] -public sealed class TitlesPlugin : PluginBase -{ - public const string Owner = "cn.Empostor.titles"; - private readonly ILocalizationRegistry _registry; - private readonly ILogger _logger; - private IDisposable? _registration; - public TitlesPlugin(ILogger logger, ILocalizationRegistry registry) { _logger = logger; _registry = registry; } - public override ValueTask EnableAsync() - { - if (!_registry.TryRegister(new LocalizationCatalog(Owner, typeof(TitlesPlugin).Assembly, "Empostor.Plugins.Titles.Localization"), out _registration)) - _logger.LogWarning("Could not register localization resources for {Owner}", Owner); - return default; - } - public override ValueTask DisableAsync() { _registration?.Dispose(); return default; } -} diff --git a/src/Empostor.Plugins.Titles/TitlesPluginStartup.cs b/src/Empostor.Plugins.Titles/TitlesPluginStartup.cs deleted file mode 100644 index b8ced79..0000000 --- a/src/Empostor.Plugins.Titles/TitlesPluginStartup.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Empostor.Api.Admin; -using Empostor.Api.Events; -using Empostor.Api.Plugins; -using Empostor.Plugins.Titles.Service; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Empostor.Plugins.Titles; - -public sealed class TitlesPluginStartup : IPluginHttpStartup -{ - private string _configPath = "[Title System]Config.json"; - - public void SetConfigPath(string configPath) => _configPath = configPath; - - public void ConfigureHost(IHostBuilder host) { } - - public void ConfigureServices(IServiceCollection services) - { - var config = PluginConfigLoader.Load(_configPath); - services.AddSingleton(config); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton( - sp => new TitlesAdminExtension(config, _configPath, sp.GetRequiredService())); - } - - public void ConfigureWebApplication(IApplicationBuilder builder) - { - builder.UseMiddleware(); - } -} diff --git a/src/Empostor.Plugins.Titles/packages.lock.json b/src/Empostor.Plugins.Titles/packages.lock.json deleted file mode 100644 index 5763b34..0000000 --- a/src/Empostor.Plugins.Titles/packages.lock.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net8.0": { - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.1", - "contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA==" - }, - "Empostor.Hazel": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "cblWCgvLcQ2xK6ztV04RVrghVEnJpCWe4N6G3LCNaJhuUq367a1/bbtQxUWAflqvgSvGwY3g3SClhh9eWTCBDA==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.1", - "Empostor.Hazel.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.6", - "Microsoft.Extensions.ObjectPool": "8.0.1", - "Serilog": "4.3.1" - } - }, - "Empostor.Hazel.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "uJqGMASOGj+ELffmIMqubyvJ/FHjOXA1RtQdS38s1LRhP+Eu4zJMTTndr12+Q3vS0kHggFFyuExFmNxWyzWmig==" - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.2.0", - "System.Text.Encodings.Web": "4.5.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "2.2.0" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.6", - "contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA==" - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g==" - }, - "empostor.api": { - "type": "Project", - "dependencies": { - "Empostor.Hazel": "[1.0.2, )", - "Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Empostor.Server/Http/TokenController.cs b/src/Empostor.Server/Http/TokenController.cs index 085cbe2..1e6d47f 100644 --- a/src/Empostor.Server/Http/TokenController.cs +++ b/src/Empostor.Server/Http/TokenController.cs @@ -78,16 +78,12 @@ public sealed class TokenController : ControllerBase return Unauthorized(new { error = "Invalid token content" }); } + _logger.LogInformation("TokenController request for eosToken={eosToken}", eosToken); + var clientIp = GetClientIp(); var friendCode = await GetFriendCodeAsync(eosToken, productUserId, clientIp); var matchmakerToken = GenerateMatchmakerToken(productUserId); - // Over quota: never take a pool port for a join that cannot succeed - // — one port per blocked request would let a single IP drain the - // whole delta range. The client is pointed at the shared reject - // port instead, whose listener answers with the localized hint: no - // game, no black screen, one socket. Port 0 (fixed-port mode) means - // the main port, which rejects it the same way. if (_rateLimit.IsLimited(clientIp, out var joinRetryAfter)) { var rejectPort = await _deltaListenerManager.GetRateLimitRejectPortAsync(); @@ -117,7 +113,7 @@ public sealed class TokenController : ControllerBase productUserId, clientIp); } - _authCache.Store(productUserId, matchmakerToken, friendCode, clientIp); + _authCache.Store(productUserId, matchmakerToken, friendCode, clientIp, eosToken); // Allocate a delta UDP port to match the TCP auth session to the // subsequent UDP connection. In fixed-port mode (pool disabled) no @@ -130,6 +126,7 @@ public sealed class TokenController : ControllerBase MatchmakerToken = matchmakerToken, FriendCode = friendCode ?? string.Empty, ClientIp = clientIp != null ? NormalizeIpString(clientIp) : null, + EosToken = eosToken, CreatedAt = DateTime.UtcNow, }; diff --git a/src/Empostor.Server/Net/ClientBase.cs b/src/Empostor.Server/Net/ClientBase.cs index 287642f..8059041 100644 --- a/src/Empostor.Server/Net/ClientBase.cs +++ b/src/Empostor.Server/Net/ClientBase.cs @@ -43,6 +43,9 @@ namespace Empostor.Server.Net /// public string? ProductUserId { get; set; } + /// + public string? EosToken { get; set; } + public GameVersion GameVersion { get; } public IHazelConnection Connection { get; } diff --git a/src/Empostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs b/src/Empostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs index 19ad273..7e2c909 100644 --- a/src/Empostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs +++ b/src/Empostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Empostor.Api.Net.Inner.Objects; namespace Empostor.Server.Net.Inner.Objects @@ -6,5 +7,7 @@ namespace Empostor.Server.Net.Inner.Objects internal partial class InnerPlayerInfo : InnerNetObject, IInnerPlayerInfo { IEnumerable IInnerPlayerInfo.Tasks => Tasks; + + string? IInnerPlayerInfo.EosToken => Game.Players.FirstOrDefault(p => p.Client.Id == ClientId)?.Client.EosToken; } } diff --git a/src/Empostor.Server/Net/Manager/ClientManager.cs b/src/Empostor.Server/Net/Manager/ClientManager.cs index 4f619bf..4a19117 100644 --- a/src/Empostor.Server/Net/Manager/ClientManager.cs +++ b/src/Empostor.Server/Net/Manager/ClientManager.cs @@ -223,6 +223,7 @@ namespace Empostor.Server.Net.Manager var client = _clientFactory.Create(connection, name, clientVersion, language, chatMode, platformSpecificData); client.FriendCode = string.IsNullOrEmpty(friendCode) ? null : friendCode; client.ProductUserId = authInfo?.ProductUserId; + client.EosToken = authInfo?.EosToken; client.DeltaPort = deltaPort; client.Id = id; diff --git a/src/Empostor.Server/Net/State/ClientPlayer.cs b/src/Empostor.Server/Net/State/ClientPlayer.cs index 72fde4c..5fe1a4a 100644 --- a/src/Empostor.Server/Net/State/ClientPlayer.cs +++ b/src/Empostor.Server/Net/State/ClientPlayer.cs @@ -27,6 +27,8 @@ namespace Empostor.Server.Net.State public ClientBase Client { get; } + public string? EosToken => Client.EosToken; + public Game Game { get; } public LimboStates Limbo { get; set; } diff --git a/src/Empostor.Server/Service/Auth/AuthCacheService.cs b/src/Empostor.Server/Service/Auth/AuthCacheService.cs index 9fdc156..06aec79 100644 --- a/src/Empostor.Server/Service/Auth/AuthCacheService.cs +++ b/src/Empostor.Server/Service/Auth/AuthCacheService.cs @@ -35,7 +35,7 @@ public sealed class AuthCacheService : IDisposable TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); } - public void Store(string productUserId, string matchmakerToken, string? friendCode, IPAddress? clientIp) + public void Store(string productUserId, string matchmakerToken, string? friendCode, IPAddress? clientIp, string? eosToken = null) { if (string.IsNullOrEmpty(productUserId) || string.IsNullOrEmpty(matchmakerToken)) { @@ -48,6 +48,7 @@ public sealed class AuthCacheService : IDisposable MatchmakerToken = matchmakerToken, FriendCode = friendCode ?? string.Empty, ClientIp = clientIp != null ? NormalizeIp(clientIp) : null, + EosToken = eosToken, CreatedAt = DateTime.UtcNow, }; @@ -162,6 +163,8 @@ public sealed class UserAuthInfo public string FriendCode { get; set; } = string.Empty; + public string? EosToken { get; set; } + public string Name { get; set; } = string.Empty; public string? ClientIp { get; set; }