diff --git a/Content.Client/_NF/M_Emp/UI/M_EmpBoundUserInterface.cs b/Content.Client/_NF/M_Emp/UI/M_EmpBoundUserInterface.cs
new file mode 100644
index 00000000000..6b758fde9c9
--- /dev/null
+++ b/Content.Client/_NF/M_Emp/UI/M_EmpBoundUserInterface.cs
@@ -0,0 +1,56 @@
+using Content.Shared._NF.M_Emp;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._NF.M_Emp.UI
+{
+ [UsedImplicitly]
+ public sealed class M_EmpBoundUserInterface : BoundUserInterface
+ {
+ private M_EmpWindow? _window;
+
+ public M_EmpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new M_EmpWindow(this);
+ _window.OnClose += Close;
+ _window.OpenCentered();
+ }
+
+ ///
+ /// Update the ui each time new state data is sent from the server.
+ ///
+ ///
+ /// Data of the that this ui represents.
+ /// Sent from the server.
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ // var castState = (M_EmpBoundUserInterfaceState) state;
+ // _window?.UpdateState(castState); //Update window state
+ }
+
+ public void ButtonPressed(UiButton button)
+ {
+ SendMessage(new UiButtonPressedMessage(button));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
+ }
+ }
+}
+
diff --git a/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml b/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml
new file mode 100644
index 00000000000..cba7f432053
--- /dev/null
+++ b/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml.cs b/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml.cs
new file mode 100644
index 00000000000..dfc52342609
--- /dev/null
+++ b/Content.Client/_NF/M_Emp/UI/M_EmpWindow.xaml.cs
@@ -0,0 +1,39 @@
+using Content.Client.UserInterface;
+using Content.Shared._NF.M_Emp;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._NF.M_Emp.UI
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class M_EmpWindow : DefaultWindow
+ {
+ public M_EmpWindow(M_EmpBoundUserInterface ui)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ RequestButton.OnPressed += _ => ui.ButtonPressed(UiButton.Request);
+ ActivateButton.OnPressed += _ => ui.ButtonPressed(UiButton.Activate);
+ }
+
+ ///
+ /// Update the UI state when new state data is received from the server.
+ ///
+ /// State data sent by the server.
+ public void UpdateState(BoundUserInterfaceState state)
+ {
+ var castState = (M_EmpBoundUserInterfaceState) state;
+
+ // Disable all buttons if not powered
+ if (Contents.Children != null)
+ {
+ ButtonHelpers.SetButtonDisabledRecursive(Contents, !castState.HasPower);
+ }
+
+ //CoreCount.Text = $"{castState.CoreCount}";
+ //InjectionAmount.Text = $"{castState.InjectionAmount}";
+ }
+ }
+}
diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs
index 8248971c492..cb984897c8b 100644
--- a/Content.Server/Emp/EmpSystem.cs
+++ b/Content.Server/Emp/EmpSystem.cs
@@ -6,6 +6,7 @@
using Content.Shared.Emp;
using Content.Shared.Examine;
using Robust.Shared.Map;
+using static Content.Server.Shuttles.Systems.ThrusterSystem;
namespace Content.Server.Emp;
@@ -26,6 +27,7 @@ public override void Initialize()
SubscribeLocalEvent(OnRadioReceiveAttempt);
SubscribeLocalEvent(OnApcToggleMainBreaker);
SubscribeLocalEvent(OnCameraSetActive);
+ SubscribeLocalEvent(OnThrusterToggle);
}
public void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
@@ -99,6 +101,11 @@ private void OnCameraSetActive(EntityUid uid, EmpDisabledComponent component, re
{
args.Cancelled = true;
}
+
+ private void OnThrusterToggle(EntityUid uid, EmpDisabledComponent component, ref ThrusterToggleAttemptEvent args)
+ {
+ args.Cancelled = true;
+ }
}
[ByRefEvent]
diff --git a/Content.Server/Fax/FaxMachineComponent.cs b/Content.Server/Fax/FaxMachineComponent.cs
index bdc97bc2450..a42f399621f 100644
--- a/Content.Server/Fax/FaxMachineComponent.cs
+++ b/Content.Server/Fax/FaxMachineComponent.cs
@@ -15,6 +15,13 @@ public sealed class FaxMachineComponent : Component
[DataField("name")]
public string FaxName { get; set; } = "Unknown";
+ ///
+ /// If true, will sync fax name with a station name.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("useStationName")]
+ public bool UseStationName { get; set; }
+
///
/// Device address of fax in network to which data will be send
///
diff --git a/Content.Server/Fax/FaxSystem.cs b/Content.Server/Fax/FaxSystem.cs
index 26df47ce1b6..ccb48585b30 100644
--- a/Content.Server/Fax/FaxSystem.cs
+++ b/Content.Server/Fax/FaxSystem.cs
@@ -223,6 +223,10 @@ private void OnInteractUsing(EntityUid uid, FaxMachineComponent component, Inter
component.FaxName = newName;
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-set"), uid);
UpdateUserInterface(uid, component);
+
+ // if we changed our fax name manually
+ // it will loose sync with station name
+ component.UseStationName = false;
});
args.Handled = true;
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/Mail/Components/MailComponent.cs b/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
index b3c8b188047..c89e14a5669 100644
--- a/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
+++ b/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
@@ -16,6 +16,10 @@ public sealed class MailComponent : SharedMailComponent
[DataField("recipientJob")]
public string RecipientJob = "None";
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("recipientStation")]
+ public string RecipientStation = "None";
+
// Why do we not use LockComponent?
// Because this can't be locked again,
// and we have special conditions for unlocking,
diff --git a/Content.Server/Nyanotrasen/Mail/MailSystem.cs b/Content.Server/Nyanotrasen/Mail/MailSystem.cs
index 01742b5dbc9..ba9d1b3198a 100644
--- a/Content.Server/Nyanotrasen/Mail/MailSystem.cs
+++ b/Content.Server/Nyanotrasen/Mail/MailSystem.cs
@@ -21,9 +21,12 @@
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
+using Content.Server.Station.Components;
using Content.Server.Station.Systems;
+using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
+using Content.Shared.Coordinates;
using Content.Shared.Damage;
using Content.Shared.Emag.Components;
using Content.Shared.Destructible;
@@ -60,6 +63,7 @@ public sealed class MailSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly StationRecordsSystem _recordsSystem = default!;
private ISawmill _sawmill = default!;
@@ -214,7 +218,7 @@ private void OnExamined(EntityUid uid, MailComponent component, ExaminedEvent ar
return;
}
- args.PushMarkup(Loc.GetString("mail-desc-close", ("name", component.Recipient), ("job", component.RecipientJob)));
+ args.PushMarkup(Loc.GetString("mail-desc-close", ("name", component.Recipient), ("job", component.RecipientJob), ("station", component.RecipientStation)));
if (component.IsFragile)
args.PushMarkup(Loc.GetString("mail-desc-fragile"));
@@ -444,7 +448,7 @@ public void SetupMail(EntityUid uid, MailTeleporterComponent component, MailReci
mailComp.RecipientJob = recipient.Job;
mailComp.Recipient = recipient.Name;
-
+ mailComp.RecipientStation = recipient.Ship;
if (mailComp.IsFragile)
{
mailComp.Bounty += component.FragileBonus;
@@ -536,6 +540,20 @@ public bool TryGetMailRecipientForReceiver(MailReceiverComponent receiver, [NotN
HashSet accessTags = access.Tags;
var mayReceivePriorityMail = true;
+ var stationUid = _stationSystem.GetOwningStation(receiver.Owner);
+ var stationName = string.Empty;
+ if (stationUid is EntityUid station
+ && TryComp(station, out var stationData)
+ && _stationSystem.GetLargestGrid(stationData) is EntityUid stationGrid
+ && TryName(stationGrid, out var gridName)
+ && gridName != null)
+ {
+ stationName = gridName;
+ }
+ else
+ {
+ stationName = "Unknown";
+ }
if (TryComp(receiver.Owner, out MindContainerComponent? mind)
&& mind.Mind?.Session == null)
@@ -546,7 +564,8 @@ public bool TryGetMailRecipientForReceiver(MailReceiverComponent receiver, [NotN
recipient = new MailRecipient(idCard.FullName,
idCard.JobTitle,
accessTags,
- mayReceivePriorityMail);
+ mayReceivePriorityMail,
+ stationName);
return true;
}
@@ -561,12 +580,16 @@ public bool TryGetMailRecipientForReceiver(MailReceiverComponent receiver, [NotN
public List GetMailRecipientCandidates(EntityUid uid)
{
List candidateList = new();
-
+ var mailLocation = Transform(uid);
foreach (var receiver in EntityQuery())
{
// mail is mapwide now, dont need to check if they are on the same station
//if (_stationSystem.GetOwningStation(receiver.Owner) != _stationSystem.GetOwningStation(uid))
// continue;
+ var location = Transform(receiver.Owner);
+
+ if (location.MapID != mailLocation.MapID)
+ continue;
if (TryGetMailRecipientForReceiver(receiver, out MailRecipient? recipient))
candidateList.Add(recipient.Value);
@@ -700,13 +723,15 @@ public struct MailRecipient
public string Job;
public HashSet AccessTags;
public bool MayReceivePriorityMail;
+ public string Ship;
- public MailRecipient(string name, string job, HashSet accessTags, bool mayReceivePriorityMail)
+ public MailRecipient(string name, string job, HashSet accessTags, bool mayReceivePriorityMail, string ship)
{
Name = name;
Job = job;
AccessTags = accessTags;
MayReceivePriorityMail = mayReceivePriorityMail;
+ Ship = ship;
}
}
}
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