diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 1901c4e60b..0d1025b803 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,6 +1,7 @@ using Content.Server._Monkestation; using Content.Server._Monkestation.Administration.Managers; using Content.Server._Monkestation.Announcements; +using Content.Server._Monkestation.Discord.Commands; using Content.Server.Administration; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -89,6 +90,7 @@ public static void Register(IDependencyCollection deps) // Monkestation start deps.Register(); deps.Register(); + deps.Register(); // Monkestation end } } diff --git a/Content.Server/_Monkestation/Discord/Commands/StatusDiscordCommand.cs b/Content.Server/_Monkestation/Discord/Commands/StatusDiscordCommand.cs new file mode 100644 index 0000000000..9b5a4f7b6f --- /dev/null +++ b/Content.Server/_Monkestation/Discord/Commands/StatusDiscordCommand.cs @@ -0,0 +1,98 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Administration.Managers; +using Content.Server.Discord.DiscordLink; +using Content.Server.GameTicking; +using Content.Server.Maps; +using Content.Shared.CCVar; +using NetCord; +using NetCord.Rest; +using Robust.Server; +using Robust.Server.Player; +using Robust.Shared.Asynchronous; +using Robust.Shared.Configuration; + +namespace Content.Server._Monkestation.Discord.Commands; + +public sealed partial class StatusDiscordCommand : IPostInjectInit +{ + [Dependency] private DiscordLink _discordLink = default!; + [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private IPlayerManager _playerManager = default!; + [Dependency] private IAdminManager _adminManager = default!; + [Dependency] private IBaseServer _baseServer = default!; + [Dependency] private IGameMapManager _gameMapManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private ITaskManager _taskManager = default!; + + public void PostInject() + { + _discordLink.RegisterCommandCallback(OnStatusCommand, "status"); + } + + private async void OnStatusCommand(CommandReceivedEventArgs args) + { + var channelId = args.Message.Channel?.Id; + if (channelId == null) + return; + var embed = await RunOnMainThread(() => + { + var gameTicker = _entityManager.System(); + + List rows = []; + + var playerCount = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount) + ? _playerManager.PlayerCount + : _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count(); + var playerCap = _cfg.GetCVar(CCVars.SoftMaxPlayers); + rows.Add(Loc.GetString("ms-discord-cmd-status-playercount", ("players", playerCount), ("maxPlayers", playerCap))); + + var mapName = _gameMapManager.GetSelectedMap()?.MapName; + if (mapName != null) + rows.Add(Loc.GetString("ms-discord-cmd-status-mapname", ("map", mapName))); + + var roundId = gameTicker.RoundId; + rows.Add(Loc.GetString("ms-discord-cmd-status-roundid", ("round", roundId))); + + switch (gameTicker.RunLevel) + { + case GameRunLevel.PreRoundLobby: + rows.Add(Loc.GetString("ms-discord-cmd-status-runlevel-preround")); + break; + case GameRunLevel.InRound: + rows.Add(Loc.GetString("ms-discord-cmd-status-runlevel-inround", ("time", TimeSpan.FromSeconds(Math.Round(gameTicker.RoundDuration().TotalSeconds))))); + break; + case GameRunLevel.PostRound: + rows.Add(Loc.GetString("ms-discord-cmd-status-runlevel-postround")); + break; + } + + return new EmbedProperties() + .WithTitle(_baseServer.ServerName) + .WithDescription(string.Join("\n", rows)) + .WithColor(new NetCord.Color(0, 255, 0)); + + }); + + await _discordLink.SendEmbedAsync(channelId.Value, embed); + } + + private async Task RunOnMainThread(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + _taskManager.RunOnMainThread(() => + { + try + { + taskCompletionSource.TrySetResult(func()); + } + catch (Exception e) + { + taskCompletionSource.TrySetException(e); + } + }); + + var result = await taskCompletionSource.Task; + return result; + } +} diff --git a/Content.Server/_Monkestation/Discord/DiscordLink/DiscorLink.Monke.cs b/Content.Server/_Monkestation/Discord/DiscordLink/DiscorLink.Monke.cs new file mode 100644 index 0000000000..fa69eb8f2f --- /dev/null +++ b/Content.Server/_Monkestation/Discord/DiscordLink/DiscorLink.Monke.cs @@ -0,0 +1,33 @@ +using NetCord; +using NetCord.Rest; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace Content.Server.Discord.DiscordLink; + +public sealed partial class DiscordLink +{ + /// + /// Sends an embed to a Discord channel with the specified ID. Without any mentions. + /// + public async Task SendEmbedAsync(ulong channelId, EmbedProperties embed) + { + if (_client == null) + { + return; + } + + var channel = await _client.Rest.GetChannelAsync(channelId) as TextChannel; + if (channel == null) + { + _sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel); + return; + } + + await channel.SendMessageAsync(new MessageProperties() + { + AllowedMentions = AllowedMentionsProperties.None, + Embeds = [embed], + }); + } +} diff --git a/Resources/Locale/en-US/_Monkestation/Discord/commands.ftl b/Resources/Locale/en-US/_Monkestation/Discord/commands.ftl new file mode 100644 index 0000000000..09679b4ef2 --- /dev/null +++ b/Resources/Locale/en-US/_Monkestation/Discord/commands.ftl @@ -0,0 +1,6 @@ +ms-discord-cmd-status-playercount = **Players**: {$players}/{$maxPlayers} +ms-discord-cmd-status-mapname = **Map**: {$map} +ms-discord-cmd-status-roundid = **Round#** {$round} +ms-discord-cmd-status-runlevel-preround = **Status**: Lobby +ms-discord-cmd-status-runlevel-inround = **Round Time**: {$time} +ms-discord-cmd-status-runlevel-postround = **Status**: Round End