From e52d533b2c4214cc9c41eafec69d327a2d34fd79 Mon Sep 17 00:00:00 2001 From: rhailrake <49613070+rhailrake@users.noreply.github.com> Date: Tue, 25 Apr 2023 20:11:23 +0600 Subject: [PATCH] [feat] Discord hooks # Conflicts: # Content.Server/GameTicking/GameTicker.RoundFlow.cs --- .../GameTicking/GameTicker.RoundFlow.cs | 44 ++---- .../Discord/GameTicking/RoundEndedEvent.cs | 13 ++ .../Discord/GameTicking/RoundStartedEvent.cs | 11 ++ .../White/Discord/RoundNotificationsSystem.cs | 125 ++++++++++++++++++ Content.Shared/White/WhiteCVars.cs | 22 +++ 5 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 Content.Server/White/Discord/GameTicking/RoundEndedEvent.cs create mode 100644 Content.Server/White/Discord/GameTicking/RoundStartedEvent.cs create mode 100644 Content.Server/White/Discord/RoundNotificationsSystem.cs diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 081cf533cd..bc8788646a 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Linq; using Content.Server.Announcements; using Content.Server.Discord; @@ -158,7 +159,10 @@ namespace Content.Server.GameTicking var ev = new PreGameMapLoad(targetMapId, map, loadOpts); RaiseLocalEvent(ev); - var gridIds = _map.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options); + if (!_map.TryLoad(targetMapId, ev.GameMap.MapPath.ToString(), out var gridIds, ev.Options)) + { + return new Collection(); + } _metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), "Station map"); @@ -250,7 +254,7 @@ namespace Content.Server.GameTicking UpdateLateJoinStatus(); AnnounceRound(); UpdateInfoText(); - SendRoundStartedDiscordMessage(); + RaiseLocalEvent(new RoundStartedEvent(RoundId)); // WD-EDIT #if EXCEPTION_TOLERANCE } @@ -304,7 +308,6 @@ namespace Content.Server.GameTicking LobbySong = _robustRandom.Pick(_lobbyMusicCollection.PickFiles).ToString(); ShowRoundEndScoreboard(text); - SendRoundEndDiscordMessage(); } public void ShowRoundEndScoreboard(string text = "") @@ -360,7 +363,7 @@ namespace Content.Server.GameTicking if (TryGetEntity(mind.OriginalOwnedEntity, out var entity)) { - _pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true); + _pvsOverride.AddGlobalOverride(entity.Value); } var roles = _roles.MindGetAllRoles(mindId); @@ -388,38 +391,7 @@ namespace Content.Server.GameTicking RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, RoundId, listOfPlayerInfoFinal.Length, listOfPlayerInfoFinal, LobbySong)); - } - - private async void SendRoundEndDiscordMessage() - { - try - { - if (_webhookIdentifier == null) - return; - - var duration = RoundDuration(); - var content = Loc.GetString("discord-round-notifications-end", - ("id", RoundId), - ("hours", Math.Truncate(duration.TotalHours)), - ("minutes", duration.Minutes), - ("seconds", duration.Seconds)); - var payload = new WebhookPayload { Content = content }; - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - - if (DiscordRoundEndRole == null) - return; - - content = Loc.GetString("discord-round-notifications-end-ping", ("roleId", DiscordRoundEndRole)); - payload = new WebhookPayload { Content = content }; - payload.AllowedMentions.AllowRoleMentions(); - - await _discord.CreateMessage(_webhookIdentifier.Value, payload); - } - catch (Exception e) - { - Log.Error($"Error while sending discord round end message:\n{e}"); - } + RaiseLocalEvent(new RoundEndedEvent(RoundId, roundDuration)); // WD-EDIT } public void RestartRound() diff --git a/Content.Server/White/Discord/GameTicking/RoundEndedEvent.cs b/Content.Server/White/Discord/GameTicking/RoundEndedEvent.cs new file mode 100644 index 0000000000..5ed462d085 --- /dev/null +++ b/Content.Server/White/Discord/GameTicking/RoundEndedEvent.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.GameTicking; + +public sealed class RoundEndedEvent : EntityEventArgs +{ + public int RoundId { get; } + public TimeSpan RoundDuration { get; } + + public RoundEndedEvent(int roundId, TimeSpan roundDuration) + { + RoundId = roundId; + RoundDuration = roundDuration; + } +} diff --git a/Content.Server/White/Discord/GameTicking/RoundStartedEvent.cs b/Content.Server/White/Discord/GameTicking/RoundStartedEvent.cs new file mode 100644 index 0000000000..5340f24bcf --- /dev/null +++ b/Content.Server/White/Discord/GameTicking/RoundStartedEvent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.GameTicking; + +public sealed class RoundStartedEvent : EntityEventArgs +{ + public int RoundId { get; } + + public RoundStartedEvent(int roundId) + { + RoundId = roundId; + } +} diff --git a/Content.Server/White/Discord/RoundNotificationsSystem.cs b/Content.Server/White/Discord/RoundNotificationsSystem.cs new file mode 100644 index 0000000000..3165f2ff84 --- /dev/null +++ b/Content.Server/White/Discord/RoundNotificationsSystem.cs @@ -0,0 +1,125 @@ +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Content.Server.Maps; +using Content.Shared.GameTicking; +using Content.Shared.White; +using Robust.Shared.Configuration; + +namespace Content.Server.Corvax.RoundNotifications; + +/// +/// Listen game events and send notifications to Discord +/// +public sealed class RoundNotificationsSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly IGameMapManager _gameMapManager = default!; + + private ISawmill _sawmill = default!; + private readonly HttpClient _httpClient = new(); + + private string _webhookUrl = String.Empty; + private string _roleId = String.Empty; + private bool _roundStartOnly; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnRoundRestart); + SubscribeLocalEvent(OnRoundStarted); + SubscribeLocalEvent(OnRoundEnded); + + _config.OnValueChanged(WhiteCVars.DiscordRoundWebhook, value => _webhookUrl = value, true); + _config.OnValueChanged(WhiteCVars.DiscordRoundRoleId, value => _roleId = value, true); + _config.OnValueChanged(WhiteCVars.DiscordRoundStartOnly, value => _roundStartOnly = value, true); + + _sawmill = IoCManager.Resolve().GetSawmill("notifications"); + } + + private void OnRoundRestart(RoundRestartCleanupEvent e) + { + if (String.IsNullOrEmpty(_webhookUrl)) + return; + + var payload = new WebhookPayload() + { + Content = Loc.GetString("discord-round-new"), + }; + + if (!String.IsNullOrEmpty(_roleId)) + { + payload = new WebhookPayload() + { + Content = $"<@&{_roleId}> {Loc.GetString("discord-round-new")}", + AllowedMentions = new Dictionary + { + { "roles", new []{ _roleId } } + }, + }; + } + + SendDiscordMessage(payload); + } + + private void OnRoundStarted(RoundStartedEvent e) + { + if (String.IsNullOrEmpty(_webhookUrl)) + return; + + var map = _gameMapManager.GetSelectedMap(); + var mapName = map?.MapName ?? Loc.GetString("discord-round-unknown-map"); + var text = Loc.GetString("discord-round-start", + ("id", e.RoundId), + ("map", mapName)); + var payload = new WebhookPayload() { Content = text }; + + SendDiscordMessage(payload); + } + + private void OnRoundEnded(RoundEndedEvent e) + { + if (String.IsNullOrEmpty(_webhookUrl) || _roundStartOnly) + return; + + var text = Loc.GetString("discord-round-end", + ("id", e.RoundId), + ("hours", e.RoundDuration.Hours), + ("minutes", e.RoundDuration.Minutes), + ("seconds", e.RoundDuration.Seconds)); + var payload = new WebhookPayload() { Content = text }; + + SendDiscordMessage(payload); + } + + private async void SendDiscordMessage(WebhookPayload payload) + { + var request = await _httpClient.PostAsync(_webhookUrl, + new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")); + + var content = await request.Content.ReadAsStringAsync(); + if (!request.IsSuccessStatusCode) + { + _sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting message: {request.StatusCode}\nResponse: {content}"); + return; + } + } + + private struct WebhookPayload + { + [JsonPropertyName("content")] + public string Content { get; set; } = ""; + + [JsonPropertyName("allowed_mentions")] + public Dictionary AllowedMentions { get; set; } = + new() + { + { "parse", Array.Empty() } + }; + + public WebhookPayload() + { + } + } +} diff --git a/Content.Shared/White/WhiteCVars.cs b/Content.Shared/White/WhiteCVars.cs index 60de439d54..ae2ae4bc75 100644 --- a/Content.Shared/White/WhiteCVars.cs +++ b/Content.Shared/White/WhiteCVars.cs @@ -34,4 +34,26 @@ public sealed class WhiteCVars QueueEnabled = CVarDef.Create("queue.enabled", false, CVar.SERVERONLY); + /* + * RoundNotifications + */ + + /// + /// URL of the Discord webhook which will send round status notifications. + /// + public static readonly CVarDef DiscordRoundWebhook = + CVarDef.Create("discord.round_webhook", string.Empty, CVar.SERVERONLY); + + /// + /// Discord ID of role which will be pinged on new round start message. + /// + public static readonly CVarDef DiscordRoundRoleId = + CVarDef.Create("discord.round_roleid", string.Empty, CVar.SERVERONLY); + + /// + /// Send notifications only about a new round begins. + /// + public static readonly CVarDef DiscordRoundStartOnly = + CVarDef.Create("discord.round_start_only", false, CVar.SERVERONLY); + }