From 18d87048b2a112effffd3c68ef6743db5d865f42 Mon Sep 17 00:00:00 2001 From: linmeideli <2558527373@qq.com> Date: Sun, 18 Oct 2026 09:32:08 +0800 Subject: [PATCH] Add EOSToken interface --- marketplace/Empostor.Plugin.Narrator.dll | Bin 48640 -> 0 bytes marketplace/Empostor.Plugins.MapVote.dll | Bin 29696 -> 0 bytes marketplace/Empostor.Plugins.PlayerLog.dll | Bin 28672 -> 0 bytes marketplace/Empostor.Plugins.Titles.dll | Bin 25088 -> 0 bytes marketplace/plugins.json | 52 --- src/Empostor.Api/Net/IClient.cs | 5 + src/Empostor.Api/Net/IClientPlayer.cs | 5 + .../Net/Inner/Objects/IInnerPlayerInfo.cs | 5 + .../Empostor.Plugin.Narrator.csproj | 13 - .../Localization/en.json | 16 - .../Localization/zh-CN.json | 16 - .../NarratorAdminExtension.cs | 97 ----- .../NarratorCommand.cs | 152 ------- .../NarratorConfig.cs | 21 - .../NarratorEventListener.cs | 82 ---- .../NarratorPlugin.cs | 24 -- .../NarratorService.cs | 375 ------------------ .../NarratorStartup.cs | 32 -- .../packages.lock.json | 140 ------- .../Empostor.Plugins.MapVote.csproj | 13 - .../Localization/en.json | 20 - .../Localization/zh-CN.json | 20 - .../MapVoteCommand.cs | 191 --------- src/Empostor.Plugins.MapVote/MapVoteConfig.cs | 15 - .../MapVoteEventListener.cs | 60 --- src/Empostor.Plugins.MapVote/MapVotePlugin.cs | 24 -- .../MapVoteService.cs | 137 ------- .../MapVoteStartup.cs | 26 -- .../packages.lock.json | 140 ------- .../Empostor.Plugins.PlayerLog.csproj | 13 - .../PlayerLogAdminExtension.cs | 176 -------- .../PlayerLogListener.cs | 96 ----- .../PlayerLogPlugin.cs | 24 -- .../PlayerLogPluginStartup.cs | 61 --- .../PlayerLogStore.cs | 162 -------- .../packages.lock.json | 140 ------- .../Empostor.Plugins.Titles.csproj | 13 - .../Localization/en.json | 6 - .../Localization/zh-CN.json | 6 - .../Service/FriendCodeTitleListener.cs | 78 ---- .../Service/TitleApiMiddleware.cs | 108 ----- .../Service/TitleEventListener.cs | 55 --- src/Empostor.Plugins.Titles/TitleStore.cs | 28 -- .../TitlesAdminExtension.cs | 68 ---- src/Empostor.Plugins.Titles/TitlesConfig.cs | 22 - src/Empostor.Plugins.Titles/TitlesPlugin.cs | 24 -- .../TitlesPluginStartup.cs | 35 -- .../packages.lock.json | 140 ------- src/Empostor.Server/Http/TokenController.cs | 11 +- src/Empostor.Server/Net/ClientBase.cs | 3 + .../Net/Inner/Objects/InnerPlayerInfo.Api.cs | 3 + .../Net/Manager/ClientManager.cs | 1 + src/Empostor.Server/Net/State/ClientPlayer.cs | 2 + .../Service/Auth/AuthCacheService.cs | 5 +- 54 files changed, 32 insertions(+), 2929 deletions(-) delete mode 100644 marketplace/Empostor.Plugin.Narrator.dll delete mode 100644 marketplace/Empostor.Plugins.MapVote.dll delete mode 100644 marketplace/Empostor.Plugins.PlayerLog.dll delete mode 100644 marketplace/Empostor.Plugins.Titles.dll delete mode 100644 src/Empostor.Plugin.Narrator/Empostor.Plugin.Narrator.csproj delete mode 100644 src/Empostor.Plugin.Narrator/Localization/en.json delete mode 100644 src/Empostor.Plugin.Narrator/Localization/zh-CN.json delete mode 100644 src/Empostor.Plugin.Narrator/NarratorAdminExtension.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorCommand.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorConfig.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorEventListener.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorPlugin.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorService.cs delete mode 100644 src/Empostor.Plugin.Narrator/NarratorStartup.cs delete mode 100644 src/Empostor.Plugin.Narrator/packages.lock.json delete mode 100644 src/Empostor.Plugins.MapVote/Empostor.Plugins.MapVote.csproj delete mode 100644 src/Empostor.Plugins.MapVote/Localization/en.json delete mode 100644 src/Empostor.Plugins.MapVote/Localization/zh-CN.json delete mode 100644 src/Empostor.Plugins.MapVote/MapVoteCommand.cs delete mode 100644 src/Empostor.Plugins.MapVote/MapVoteConfig.cs delete mode 100644 src/Empostor.Plugins.MapVote/MapVoteEventListener.cs delete mode 100644 src/Empostor.Plugins.MapVote/MapVotePlugin.cs delete mode 100644 src/Empostor.Plugins.MapVote/MapVoteService.cs delete mode 100644 src/Empostor.Plugins.MapVote/MapVoteStartup.cs delete mode 100644 src/Empostor.Plugins.MapVote/packages.lock.json delete mode 100644 src/Empostor.Plugins.PlayerLog/Empostor.Plugins.PlayerLog.csproj delete mode 100644 src/Empostor.Plugins.PlayerLog/PlayerLogAdminExtension.cs delete mode 100644 src/Empostor.Plugins.PlayerLog/PlayerLogListener.cs delete mode 100644 src/Empostor.Plugins.PlayerLog/PlayerLogPlugin.cs delete mode 100644 src/Empostor.Plugins.PlayerLog/PlayerLogPluginStartup.cs delete mode 100644 src/Empostor.Plugins.PlayerLog/PlayerLogStore.cs delete mode 100644 src/Empostor.Plugins.PlayerLog/packages.lock.json delete mode 100644 src/Empostor.Plugins.Titles/Empostor.Plugins.Titles.csproj delete mode 100644 src/Empostor.Plugins.Titles/Localization/en.json delete mode 100644 src/Empostor.Plugins.Titles/Localization/zh-CN.json delete mode 100644 src/Empostor.Plugins.Titles/Service/FriendCodeTitleListener.cs delete mode 100644 src/Empostor.Plugins.Titles/Service/TitleApiMiddleware.cs delete mode 100644 src/Empostor.Plugins.Titles/Service/TitleEventListener.cs delete mode 100644 src/Empostor.Plugins.Titles/TitleStore.cs delete mode 100644 src/Empostor.Plugins.Titles/TitlesAdminExtension.cs delete mode 100644 src/Empostor.Plugins.Titles/TitlesConfig.cs delete mode 100644 src/Empostor.Plugins.Titles/TitlesPlugin.cs delete mode 100644 src/Empostor.Plugins.Titles/TitlesPluginStartup.cs delete mode 100644 src/Empostor.Plugins.Titles/packages.lock.json diff --git a/marketplace/Empostor.Plugin.Narrator.dll b/marketplace/Empostor.Plugin.Narrator.dll deleted file mode 100644 index 89c6238a05763c2374c93536117f6126e35a2b79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48640 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 diff --git a/marketplace/Empostor.Plugins.MapVote.dll b/marketplace/Empostor.Plugins.MapVote.dll deleted file mode 100644 index af1a6f31ff8c76e177fe0e60db681cb19ae285e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29696 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 diff --git a/marketplace/Empostor.Plugins.Titles.dll b/marketplace/Empostor.Plugins.Titles.dll deleted file mode 100644 index 4ac04fbee5a6799c78e43c2c02d750b3cb562390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25088 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 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; }