diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index fff054bcd73..8485116e3db 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -237,6 +237,7 @@ public void StartRound(bool force = false)
UpdateLateJoinStatus();
AnnounceRound();
UpdateInfoText();
+ RaiseLocalEvent(new RoundStartedEvent(RoundId));
#if EXCEPTION_TOLERANCE
}
@@ -359,6 +360,7 @@ public void ShowRoundEndScoreboard(string text = "")
RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, RoundId,
listOfPlayerInfoFinal.Length, listOfPlayerInfoFinal, LobbySong,
new SoundCollectionSpecifier("RoundEnd").GetSound()));
+ RaiseLocalEvent(new RoundEndedEvent(RoundId, roundDuration));
}
public void RestartRound()
diff --git a/Content.Server/Nyanotrasen/GameTicking/RoundEndedEvent.cs b/Content.Server/Nyanotrasen/GameTicking/RoundEndedEvent.cs
new file mode 100644
index 00000000000..360d4dd22c9
--- /dev/null
+++ b/Content.Server/Nyanotrasen/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/Nyanotrasen/RoundNotifications/RoundNotificationsSystem.cs b/Content.Server/Nyanotrasen/RoundNotifications/RoundNotificationsSystem.cs
new file mode 100644
index 00000000000..daaf75a4372
--- /dev/null
+++ b/Content.Server/Nyanotrasen/RoundNotifications/RoundNotificationsSystem.cs
@@ -0,0 +1,179 @@
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Content.Shared.CCVar;
+using Content.Server.Maps;
+using Content.Shared.GameTicking;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+
+namespace Content.Server.Nyanotrasen.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;
+ private string _serverName = string.Empty;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnRoundRestart);
+ SubscribeLocalEvent(OnRoundStarted);
+ SubscribeLocalEvent(OnRoundEnded);
+
+ _config.OnValueChanged(CCVars.DiscordRoundWebhook, value => _webhookUrl = value, true);
+ _config.OnValueChanged(CCVars.DiscordRoundRoleId, value => _roleId = value, true);
+ _config.OnValueChanged(CCVars.DiscordRoundStartOnly, value => _roundStartOnly = value, true);
+ _config.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
+
+ _sawmill = IoCManager.Resolve().GetSawmill("notifications");
+ }
+
+ private void OnServerNameChanged(string obj)
+ {
+ _serverName = obj;
+ }
+
+ private void OnRoundRestart(RoundRestartCleanupEvent e)
+ {
+ if (String.IsNullOrEmpty(_webhookUrl))
+ return;
+
+ var text = Loc.GetString("discord-round-new");
+
+ SendDiscordMessage(text, true, 0x91B2C7);
+ }
+
+ 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));
+
+ SendDiscordMessage(text, false);
+ }
+
+ private void OnRoundEnded(RoundEndedEvent e)
+ {
+ if (String.IsNullOrEmpty(_webhookUrl) || _roundStartOnly)
+ return;
+
+ var text = Loc.GetString("discord-round-end",
+ ("id", e.RoundId),
+ ("hours", Math.Truncate(e.RoundDuration.TotalHours)),
+ ("minutes", e.RoundDuration.Minutes),
+ ("seconds", e.RoundDuration.Seconds));
+
+ SendDiscordMessage(text, false, 0xB22B27);
+ }
+
+ private async void SendDiscordMessage(string text, bool ping = false, int color = 0x41F097)
+ {
+
+ // Limit server name to 1500 characters, in case someone tries to be a little funny
+ var serverName = _serverName[..Math.Min(_serverName.Length, 1500)];
+ var message = "";
+ if (!String.IsNullOrEmpty(_roleId) && ping)
+ message = $"<@&{_roleId}>";
+
+ // Build the embed
+ var payload = new WebhookPayload
+ {
+ Message = message,
+ Embeds = new List