]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add feedback popups (#41352)
authorJulian Giebel <juliangiebel@live.de>
Thu, 22 Jan 2026 22:19:54 +0000 (23:19 +0100)
committerGitHub <noreply@github.com>
Thu, 22 Jan 2026 22:19:54 +0000 (22:19 +0000)
* Commit

* add the form post

* dv

* fixes

* Change wording

* Address review

* wording change

* Added some stuff

* New format

* bruh

* thanks perry!

* yes

* More fixes!

* typo

* Add a command to show the list, improve the UI slightly, split up command names

* Fix UI controller

* Add better comment

* Get rid of weird recursive thing

* Cleanup

* Work on moving feedback popups out of simulation

* Move round end screen subscription to feedback ui controller

* Finish moving feedback popups out of simulation

* Fix _ as parameter

* Clean up FeedbackPopupUIController

* Clean up commands

* Fix prototype yaml

* Fix openfeedbackpopup command description

* Update Resources/Locale/en-US/feedbackpopup/feedbackpopup.ftl

Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
* Apply suggestions from code review

Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
* Address reviews

* Address reviews

* Fix FeedbackPopupPrototype.cs using empty string instead of string.empty

* Address some more of the reviews, style nano is still trolling sadly

* Fix feedback popup styling

* Fix PopupPrototype ID field not having a setter

* Address reviews

* Add label when no feedback entries are present

Change link button to not show when no link is set

---------

Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
26 files changed:
Content.Client/Entry/EntryPoint.cs
Content.Client/FeedbackPopup/ClientFeedbackManager.cs [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackEntry.xaml [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackEntry.xaml.cs [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackPopupSheetlet.cs [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackPopupUIController.cs [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml [new file with mode: 0644]
Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml.cs [new file with mode: 0644]
Content.Client/Guidebook/Richtext/ProtodataTag.cs
Content.Client/IoC/ClientContentIoC.cs
Content.Client/Options/UI/EscapeMenu.xaml
Content.Client/Stylesheets/StyleNano.cs
Content.Client/UserInterface/Systems/EscapeMenu/EscapeUIController.cs
Content.Server/Entry/EntryPoint.cs
Content.Server/FeedbackSystem/FeedbackCommand.cs [new file with mode: 0644]
Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs [new file with mode: 0644]
Content.Server/FeedbackSystem/ServerFeedbackManager.cs [new file with mode: 0644]
Content.Server/IoC/ServerContentIoC.cs
Content.Shared/CCVar/CCVars.Feedback.cs [new file with mode: 0644]
Content.Shared/FeedbackSystem/FeedbackPopupMessage.cs [new file with mode: 0644]
Content.Shared/FeedbackSystem/FeedbackPopupPrototype.cs [new file with mode: 0644]
Content.Shared/FeedbackSystem/SharedFeedbackManager.Events.cs [new file with mode: 0644]
Content.Shared/FeedbackSystem/SharedFeedbackManager.cs [new file with mode: 0644]
Resources/Locale/en-US/escape-menu/ui/escape-menu.ftl
Resources/Locale/en-US/feedbackpopup/feedbackpopup.ftl [new file with mode: 0644]
Resources/Prototypes/FeedbackPopup/feedbackpopups.yml [new file with mode: 0644]

index 2266b30c515e53bc2de13d4ffdcc02ffd8858978..e0358d54e756af93b2f05999a5f3d49af38c52a1 100644 (file)
@@ -3,6 +3,7 @@ using Content.Client.Changelog;
 using Content.Client.Chat.Managers;
 using Content.Client.DebugMon;
 using Content.Client.Eui;
+using Content.Client.FeedbackPopup;
 using Content.Client.Fullscreen;
 using Content.Client.GameTicking.Managers;
 using Content.Client.GhostKick;
@@ -24,6 +25,7 @@ using Content.Client.UserInterface;
 using Content.Client.Viewport;
 using Content.Client.Voting;
 using Content.Shared.Ame.Components;
+using Content.Shared.FeedbackSystem;
 using Content.Shared.Gravity;
 using Content.Shared.Localizations;
 using Robust.Client;
@@ -76,6 +78,7 @@ namespace Content.Client.Entry
         [Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
         [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
         [Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
+        [Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
 
         public override void PreInit()
         {
@@ -170,6 +173,7 @@ namespace Content.Client.Entry
             _userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
             _documentParsingManager.Initialize();
             _titleWindowManager.Initialize();
+            _feedbackManager.Initialize();
 
             _baseClient.RunLevelChanged += (_, args) =>
             {
diff --git a/Content.Client/FeedbackPopup/ClientFeedbackManager.cs b/Content.Client/FeedbackPopup/ClientFeedbackManager.cs
new file mode 100644 (file)
index 0000000..a4cdf6a
--- /dev/null
@@ -0,0 +1,71 @@
+using Content.Shared.FeedbackSystem;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.FeedbackPopup;
+
+/// <inheritdoc />
+public sealed class ClientFeedbackManager : SharedFeedbackManager
+{
+    /// <summary>
+    /// A read-only set representing the currently displayed feedback popups.
+    /// </summary>
+    public IReadOnlySet<ProtoId<FeedbackPopupPrototype>> DisplayedPopups => _displayedPopups;
+
+    private readonly HashSet<ProtoId<FeedbackPopupPrototype>> _displayedPopups = [];
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        NetManager.RegisterNetMessage<FeedbackPopupMessage>(ReceivedPopupMessage);
+        NetManager.RegisterNetMessage<OpenFeedbackPopupMessage>(_ => Open());
+    }
+
+    /// <summary>
+    /// Opens the feedback popup window.
+    /// </summary>
+    public void Open()
+    {
+        InvokeDisplayedPopupsChanged(true);
+    }
+
+    /// <inheritdoc />
+    public override void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
+    {
+        if (prototypes == null || !NetManager.IsClient)
+            return;
+
+        var count = _displayedPopups.Count;
+        _displayedPopups.UnionWith(prototypes);
+        InvokeDisplayedPopupsChanged(_displayedPopups.Count > count);
+    }
+
+    /// <inheritdoc />
+    public override void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
+    {
+        if (!NetManager.IsClient)
+            return;
+
+        if (prototypes == null)
+        {
+            _displayedPopups.Clear();
+        }
+        else
+        {
+            _displayedPopups.ExceptWith(prototypes);
+        }
+
+        InvokeDisplayedPopupsChanged(false);
+    }
+
+    private void ReceivedPopupMessage(FeedbackPopupMessage message)
+    {
+        if (message.Remove)
+        {
+            Remove(message.FeedbackPrototypes);
+            return;
+        }
+
+        Display(message.FeedbackPrototypes);
+    }
+}
diff --git a/Content.Client/FeedbackPopup/FeedbackEntry.xaml b/Content.Client/FeedbackPopup/FeedbackEntry.xaml
new file mode 100644 (file)
index 0000000..9b7e6ce
--- /dev/null
@@ -0,0 +1,24 @@
+<Control xmlns="https://spacestation14.io"
+         MinHeight="100">
+    <PanelContainer StyleClasses="BackgroundPanel" ModulateSelfOverride="#2b2b31"/>
+    <BoxContainer Orientation="Vertical">
+
+        <!-- Title -->
+        <PanelContainer StyleIdentifier="FeedbackBorderThinBottom">
+            <RichTextLabel Name="TitleLabel" Margin="12 6 6 6" />
+        </PanelContainer>
+
+        <!-- Description -->
+        <RichTextLabel Name="DescriptionLabel" StyleClasses="LabelLight" Margin="12 4 12 8" VerticalExpand="True"/>
+
+        <!-- Footer -->
+        <PanelContainer StyleIdentifier="FeedbackBorderThinTop">
+            <BoxContainer>
+                <Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="TypeLabel" Margin="14 6 6 6" />
+                <Button Name="LinkButton" Text="{Loc feedbackpopup-control-button-text}" MinWidth="80"
+                        Margin="8 6 14 6" HorizontalExpand="True" HorizontalAlignment="Right" />
+            </BoxContainer>
+        </PanelContainer>
+
+    </BoxContainer>
+</Control>
diff --git a/Content.Client/FeedbackPopup/FeedbackEntry.xaml.cs b/Content.Client/FeedbackPopup/FeedbackEntry.xaml.cs
new file mode 100644 (file)
index 0000000..a3a6cd9
--- /dev/null
@@ -0,0 +1,54 @@
+using Content.Shared.FeedbackSystem;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.FeedbackPopup;
+
+[GenerateTypedNameReferences]
+public sealed partial class FeedbackEntry : Control
+{
+    private readonly IUriOpener _uri;
+
+    private readonly FeedbackPopupPrototype? _prototype;
+
+    public FeedbackEntry(ProtoId<FeedbackPopupPrototype> popupProto, IPrototypeManager proto, IUriOpener uri)
+    {
+        RobustXamlLoader.Load(this);
+        _uri = uri;
+
+        _prototype = proto.Index(popupProto);
+
+        // Title
+        TitleLabel.Text = _prototype.Title;
+        DescriptionLabel.Text = _prototype.Description;
+        TypeLabel.Text = _prototype.ResponseType;
+
+        LinkButton.Visible = !string.IsNullOrEmpty(_prototype.ResponseLink);
+
+        // link button
+        if (!string.IsNullOrEmpty(_prototype.ResponseLink))
+        {
+            LinkButton.OnPressed += OnButtonPressed;
+        }
+    }
+
+    private void OnButtonPressed(BaseButton.ButtonEventArgs args)
+    {
+        if (!string.IsNullOrWhiteSpace(_prototype?.ResponseLink))
+            _uri.OpenUri(_prototype.ResponseLink);
+    }
+
+    protected override void Resized()
+    {
+        base.Resized();
+        // magic
+        TitleLabel.SetWidth = Width - TitleLabel.Margin.SumHorizontal;
+        TitleLabel.InvalidateArrange();
+        DescriptionLabel.SetWidth = Width - DescriptionLabel.Margin.SumHorizontal;
+        DescriptionLabel.InvalidateArrange();
+    }
+}
+
diff --git a/Content.Client/FeedbackPopup/FeedbackPopupSheetlet.cs b/Content.Client/FeedbackPopup/FeedbackPopupSheetlet.cs
new file mode 100644 (file)
index 0000000..7f6d513
--- /dev/null
@@ -0,0 +1,36 @@
+using Content.Client.Stylesheets;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using static Content.Client.Stylesheets.StylesheetHelpers;
+
+namespace Content.Client.FeedbackPopup;
+
+[CommonSheetlet]
+public sealed class FeedbackPopupSheetlet : Sheetlet<PalettedStylesheet>
+{
+    public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
+    {
+        var borderTop = new StyleBoxFlat()
+        {
+            BorderColor = sheet.SecondaryPalette.Base,
+            BorderThickness = new Thickness(0, 1, 0, 0),
+        };
+
+        var borderBottom = new StyleBoxFlat()
+        {
+            BorderColor = sheet.SecondaryPalette.Base,
+            BorderThickness = new Thickness(0, 0, 0, 1),
+        };
+
+        return
+        [
+            E<PanelContainer>()
+                .Identifier("FeedbackBorderThinTop")
+                .Prop(PanelContainer.StylePropertyPanel, borderTop),
+            E<PanelContainer>()
+                .Identifier("FeedbackBorderThinBottom")
+                .Prop(PanelContainer.StylePropertyPanel, borderBottom),
+        ];
+    }
+}
diff --git a/Content.Client/FeedbackPopup/FeedbackPopupUIController.cs b/Content.Client/FeedbackPopup/FeedbackPopupUIController.cs
new file mode 100644 (file)
index 0000000..19ec95a
--- /dev/null
@@ -0,0 +1,75 @@
+using Content.Shared.FeedbackSystem;
+using Content.Shared.GameTicking;
+using Robust.Client.UserInterface.Controllers;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.FeedbackPopup;
+
+/// <summary>
+/// This handles getting feedback popup messages from the server and making a popup in the client.
+/// </summary>
+[UsedImplicitly]
+public sealed class FeedbackPopupUIController : UIController
+{
+    [Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
+    [Dependency] private readonly IPrototypeManager _proto = null!;
+    [Dependency] private readonly IUriOpener _uri = null!;
+
+    private FeedbackPopupWindow _window = null!;
+
+    public override void Initialize()
+    {
+        _window = new FeedbackPopupWindow(_proto, _uri);
+
+        SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
+        SubscribeNetworkEvent<RoundEndMessageEvent>(OnRoundEnd);
+
+        _feedbackManager.DisplayedPopupsChanged += OnPopupsChanged;
+    }
+
+    public void ToggleWindow()
+    {
+        if (_window.IsOpen)
+        {
+            _window.Close();
+        }
+        else
+        {
+            _window.OpenCentered();
+        }
+    }
+
+    private void OnRoundEnd(RoundEndMessageEvent ev, EntitySessionEventArgs args)
+    {
+        // Add round end prototypes.
+        var roundEndPrototypes = _feedbackManager.GetOriginFeedbackPrototypes(true);
+        if (roundEndPrototypes.Count == 0)
+            return;
+
+        _feedbackManager.Display(roundEndPrototypes);
+
+        // Even if no new prototypes were added, we still want to open the window.
+        if (!_window.IsOpen)
+            _window.OpenCentered();
+    }
+
+    private void OnPopupsChanged(bool newPopups)
+    {
+        UpdateWindow(_feedbackManager.DisplayedPopups);
+
+        if (newPopups && !_window.IsOpen)
+            _window.OpenCentered();
+    }
+
+    private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
+    {
+        UpdateWindow(_feedbackManager.DisplayedPopups);
+    }
+
+    private void UpdateWindow(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
+    {
+        _window.Update(prototypes);
+    }
+}
diff --git a/Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml b/Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml
new file mode 100644 (file)
index 0000000..0d17926
--- /dev/null
@@ -0,0 +1,24 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                      xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                      xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                      Title="{Loc feedbackpopup-window-name}" MinSize="510 460" RectClipContent="True">
+        <BoxContainer Orientation="Vertical">
+
+            <!-- main box area -->
+            <BoxContainer Margin="12 12 12 5" VerticalExpand="True">
+                <PanelContainer HorizontalExpand="True" StyleClasses="PanelDark">
+                    <ScrollContainer HorizontalExpand="True" HScrollEnabled="False">
+                        <BoxContainer Name="NotificationContainer" HorizontalExpand="True" Orientation="Vertical" Margin="10" SeparationOverride="10" />
+                    </ScrollContainer>
+                </PanelContainer>
+            </BoxContainer>
+
+            <!-- Footer -->
+            <BoxContainer Orientation="Vertical" SetHeight="30" Margin="2 0 0 0">
+                <BoxContainer SetHeight="33" Margin="10 0 10 5">
+                    <Label Text="{Loc feedbackpopup-control-ui-footer}" Margin="6 0" StyleClasses="PdaContentFooterText"/>
+                    <Label Name="NumNotifications" Margin="6 0" HorizontalExpand="True" HorizontalAlignment="Right"/>
+                </BoxContainer>
+            </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml.cs b/Content.Client/FeedbackPopup/FeedbackPopupWindow.xaml.cs
new file mode 100644 (file)
index 0000000..fba32eb
--- /dev/null
@@ -0,0 +1,49 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.FeedbackSystem;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.FeedbackPopup;
+
+[GenerateTypedNameReferences]
+public sealed partial class FeedbackPopupWindow : FancyWindow
+{
+    private readonly IPrototypeManager _proto;
+    private readonly IUriOpener _uri;
+
+    public FeedbackPopupWindow(IPrototypeManager proto, IUriOpener uri)
+    {
+        _proto = proto;
+        _uri = uri;
+        RobustXamlLoader.Load(this);
+        DisplayNoEntryLabel();
+    }
+
+    public void Update(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
+    {
+        NotificationContainer.RemoveAllChildren();
+
+        if (prototypes.Count == 0)
+            DisplayNoEntryLabel();
+
+        foreach (var proto in prototypes)
+        {
+            NotificationContainer.AddChild(new FeedbackEntry(proto, _proto, _uri));
+        }
+
+        NumNotifications.Text = Loc.GetString("feedbackpopup-control-total-surveys", ("num", prototypes.Count));
+    }
+
+    private void DisplayNoEntryLabel()
+    {
+        NotificationContainer.AddChild(new Label()
+        {
+            Text = Loc.GetString("feedbackpopup-control-no-entries"),
+            HorizontalAlignment = HAlignment.Center,
+            VerticalAlignment = VAlignment.Center,
+        });
+    }
+}
index 2a6eca4e485e6155dadc302800a260cf2ade7513..9d7d46e2467099ba80e5b23d8a14c10f518d0889 100644 (file)
@@ -6,7 +6,7 @@ namespace Content.Client.Guidebook.RichText;
 
 /// <summary>
 /// RichText tag that can display values extracted from entity prototypes.
-/// In order to be accessed by this tag, the desired field/property must
+/// To be accessed by this tag, the desired field/property must
 /// be tagged with <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
 /// </summary>
 public sealed class ProtodataTag : IMarkupTagHandler
index 8f81f90311db7ac1952713e900410bc18f7f13c9..efaf88b052294df44f4f8f0fd355057fd0ee7ad4 100644 (file)
@@ -4,6 +4,7 @@ using Content.Client.Chat.Managers;
 using Content.Client.Clickable;
 using Content.Client.DebugMon;
 using Content.Client.Eui;
+using Content.Client.FeedbackPopup;
 using Content.Client.Fullscreen;
 using Content.Client.GameTicking.Managers;
 using Content.Client.GhostKick;
@@ -23,6 +24,7 @@ using Content.Client.Lobby;
 using Content.Client.Players.RateLimiting;
 using Content.Shared.Administration.Managers;
 using Content.Shared.Chat;
+using Content.Shared.FeedbackSystem;
 using Content.Shared.IoC;
 using Content.Shared.Players.PlayTimeTracking;
 using Content.Shared.Players.RateLimiting;
@@ -62,6 +64,8 @@ namespace Content.Client.IoC
             collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
             collection.Register<TitleWindowManager>();
             collection.Register<ClientsidePlaytimeTrackingManager>();
+            collection.Register<ClientFeedbackManager>();
+            collection.Register<ISharedFeedbackManager, ClientFeedbackManager>();
         }
     }
 }
index 6aa14f27e700bcd2241576d6bf23ddcfa4a1ee45..f46d65ac736e8cd1d7f960f42e5801921278344a 100644 (file)
@@ -12,6 +12,7 @@
         <Button Access="Public" Name="GuidebookButton" Text="{Loc 'ui-escape-guidebook'}" />
         <Button Access="Public" Name="WikiButton" Text="{Loc 'ui-escape-wiki'}" />
         <changelog:ChangelogButton Access="Public" Name="ChangelogButton" />
+        <Button Access="Public" Name="FeedbackButton" Text="{Loc 'ui-escape-feedback'}"/>
         <PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
         <Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
         <PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
index 34195d3a7c52e844f979575e710b64700d824c3f..901b92f2c677ea8d9dbdd057cfc8b4eca5db994d 100644 (file)
@@ -1345,7 +1345,6 @@ namespace Content.Client.Stylesheets
 
                 Element<Label>().Class(StyleClassLabelSmall)
                  .Prop(Label.StylePropertyFont, notoSans10),
-                // ---
 
                 // Different Background shapes ---
                 Element<PanelContainer>().Class(ClassAngleRect)
@@ -1608,6 +1607,29 @@ namespace Content.Client.Stylesheets
                         BackgroundColor = FancyTreeSelectedRowColor,
                     }),
 
+                // Inset background (News manager, notifications)
+                Element<PanelContainer>().Class("InsetBackground")
+                    .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
+                    {
+                        BackgroundColor = Color.FromHex("#202023"),
+                    }),
+
+                // Default fancy window border styles
+                Element<PanelContainer>().Class("DefaultBorderBottom")
+                    .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
+                    {
+                        BorderColor= Color.FromHex("#3B3E56"),
+                        BorderThickness= new Thickness(0, 0, 0, 1),
+                    }),
+
+
+                Element<PanelContainer>().Class("DefaultBorderTop")
+                    .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
+                    {
+                        BorderColor= Color.FromHex("#3B3E56"),
+                        BorderThickness= new Thickness(0, 1, 0, 0),
+                    }),
+
                 // Silicon law edit ui
                 Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel)
                     .Prop(Label.StylePropertyFontColor, NanoGold),
index 85c4af767238692f1bdf60eefbb8bd7320808502..1babd0b08d05ecfa764c2f3ab92d0f28a525ece6 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Client.Gameplay;
+using Content.Client.FeedbackPopup;
+using Content.Client.Gameplay;
 using Content.Client.UserInterface.Controls;
 using Content.Client.UserInterface.Systems.Guidebook;
 using Content.Client.UserInterface.Systems.Info;
@@ -25,6 +26,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
     [Dependency] private readonly InfoUIController _info = default!;
     [Dependency] private readonly OptionsUIController _options = default!;
     [Dependency] private readonly GuidebookUIController _guidebook = default!;
+    [Dependency] private readonly FeedbackPopupUIController _feedback = null!;
 
     private Options.UI.EscapeMenu? _escapeWindow;
 
@@ -63,6 +65,12 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
         _escapeWindow.OnClose += DeactivateButton;
         _escapeWindow.OnOpen += ActivateButton;
 
+        _escapeWindow.FeedbackButton.OnPressed += _ =>
+        {
+            CloseEscapeWindow();
+            _feedback.ToggleWindow();
+        };
+
         _escapeWindow.ChangelogButton.OnPressed += _ =>
         {
             CloseEscapeWindow();
index 7ebb54687974caaa08ecd7721e21e57e11a1fd6d..4d22dfbeb0e86c27b5aa4e8511913e3e3caadb48 100644 (file)
@@ -8,6 +8,7 @@ using Content.Server.Connection;
 using Content.Server.Database;
 using Content.Server.Discord.DiscordLink;
 using Content.Server.EUI;
+using Content.Server.FeedbackSystem;
 using Content.Server.GameTicking;
 using Content.Server.GhostKick;
 using Content.Server.GuideGenerator;
@@ -23,6 +24,7 @@ using Content.Server.ServerInfo;
 using Content.Server.ServerUpdates;
 using Content.Server.Voting.Managers;
 using Content.Shared.CCVar;
+using Content.Shared.FeedbackSystem;
 using Content.Shared.Kitchen;
 using Content.Shared.Localizations;
 using Robust.Server;
@@ -76,6 +78,7 @@ namespace Content.Server.Entry
         [Dependency] private readonly ServerApi _serverApi = default!;
         [Dependency] private readonly ServerInfoManager _serverInfo = default!;
         [Dependency] private readonly ServerUpdateManager _updateManager = default!;
+        [Dependency] private readonly ServerFeedbackManager _feedbackManager = null!;
 
         public override void PreInit()
         {
@@ -165,6 +168,7 @@ namespace Content.Server.Entry
             _connection.PostInit();
             _multiServerKick.Initialize();
             _cvarCtrl.Initialize();
+            _feedbackManager.Initialize();
         }
 
         public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
diff --git a/Content.Server/FeedbackSystem/FeedbackCommand.cs b/Content.Server/FeedbackSystem/FeedbackCommand.cs
new file mode 100644 (file)
index 0000000..68caed7
--- /dev/null
@@ -0,0 +1,63 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.FeedbackSystem;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.FeedbackSystem;
+
+/// <summary>
+/// Adds, removes, and displays feedback for specified sessions.
+/// </summary>
+[ToolshedCommand]
+[AdminCommand(AdminFlags.Debug)]
+public sealed class FeedbackCommand : ToolshedCommand
+{
+    [Dependency] private readonly ISharedFeedbackManager _feedback = null!;
+
+    [CommandImplementation("show")]
+    public void ExecuteShow([CommandArgument] ICommonSession session)
+    {
+        _feedback.OpenForSession(session);
+    }
+
+    [CommandImplementation("show")]
+    public void ExecuteShow([PipedArgument] IEnumerable<ICommonSession> sessions)
+    {
+        foreach (var session in sessions)
+        {
+            _feedback.OpenForSession(session);
+        }
+    }
+
+    [CommandImplementation("add")]
+    public void ExecuteAdd([CommandArgument] ICommonSession session, ProtoId<FeedbackPopupPrototype> protoId)
+    {
+        _feedback.SendToSession(session, [protoId]);
+    }
+
+    [CommandImplementation("add")]
+    public void ExecuteAdd([PipedArgument] IEnumerable<ICommonSession> sessions, ProtoId<FeedbackPopupPrototype> protoId)
+    {
+        foreach (var session in sessions)
+        {
+            _feedback.SendToSession(session, [protoId]);
+        }
+    }
+
+    [CommandImplementation("remove")]
+    public void ExecuteRemove([CommandArgument] ICommonSession session, ProtoId<FeedbackPopupPrototype> protoId)
+    {
+        _feedback.SendToSession(session, [protoId], true);
+    }
+
+    [CommandImplementation("remove")]
+    public void ExecuteRemove([PipedArgument] IEnumerable<ICommonSession> sessions, ProtoId<FeedbackPopupPrototype> protoId)
+    {
+        foreach (var session in sessions)
+        {
+            _feedback.SendToSession(session, [protoId], true);
+        }
+    }
+}
diff --git a/Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs b/Content.Server/FeedbackSystem/OpenFeedbackPopupCommand.cs
new file mode 100644 (file)
index 0000000..7816f7d
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.Administration;
+using Content.Shared.FeedbackSystem;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.FeedbackSystem;
+
+/// <summary>
+/// Opens the feedback popup window for the executing session
+/// </summary>
+[AnyCommand]
+[ToolshedCommand]
+public sealed class OpenFeedbackPopupCommand : ToolshedCommand
+{
+    [Dependency] private readonly ISharedFeedbackManager _feedback = null!;
+
+    [CommandImplementation]
+    public void Execute(IInvocationContext context)
+    {
+        if (context.Session == null)
+            return;
+
+        _feedback.OpenForSession(context.Session);
+    }
+}
diff --git a/Content.Server/FeedbackSystem/ServerFeedbackManager.cs b/Content.Server/FeedbackSystem/ServerFeedbackManager.cs
new file mode 100644 (file)
index 0000000..09edd8e
--- /dev/null
@@ -0,0 +1,78 @@
+using Content.Shared.FeedbackSystem;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.FeedbackSystem;
+
+/// <inheritdoc />
+public sealed class ServerFeedbackManager : SharedFeedbackManager
+{
+    [Dependency] private readonly ISharedPlayerManager _player = null!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        NetManager.RegisterNetMessage<FeedbackPopupMessage>();
+        NetManager.RegisterNetMessage<OpenFeedbackPopupMessage>();
+    }
+
+    /// <inheritdoc />
+    public override bool Send(EntityUid uid, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes)
+    {
+        if (!_player.TryGetSessionByEntity(uid, out var session))
+            return false;
+
+        SendToSession(session, popupPrototypes);
+        return true;
+    }
+
+    /// <inheritdoc />
+    public override void SendToSession(ICommonSession session, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false)
+    {
+        if (!NetManager.IsServer)
+            return;
+
+        var msg = new FeedbackPopupMessage
+        {
+            FeedbackPrototypes = popupPrototypes,
+            Remove = remove,
+        };
+
+        NetManager.ServerSendMessage(msg, session.Channel);
+    }
+
+    /// <inheritdoc />
+    public override void SendToAllSessions(List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false)
+    {
+        if (!NetManager.IsServer)
+            return;
+
+        var msg = new FeedbackPopupMessage
+        {
+            FeedbackPrototypes = popupPrototypes,
+            Remove = remove,
+        };
+
+        NetManager.ServerSendToAll(msg);
+    }
+
+    /// <inheritdoc />
+    public override void OpenForSession(ICommonSession session)
+    {
+        if (!NetManager.IsServer)
+            return;
+
+        var msg = new OpenFeedbackPopupMessage();
+        NetManager.ServerSendMessage(msg, session.Channel);
+    }
+
+    /// <inheritdoc />
+    public override void OpenForAllSessions()
+    {
+        if (!NetManager.IsServer)
+            return;
+
+        var msg = new OpenFeedbackPopupMessage();
+        NetManager.ServerSendToAll(msg);
+    }
+}
index b6b5316da72dfc129d695627972e4ba0c2ccd25d..1c6d940e20fd47055021999957323aa0c626f581 100644 (file)
@@ -10,6 +10,7 @@ using Content.Server.Discord;
 using Content.Server.Discord.DiscordLink;
 using Content.Server.Discord.WebhookMessages;
 using Content.Server.EUI;
+using Content.Server.FeedbackSystem;
 using Content.Server.GhostKick;
 using Content.Server.Info;
 using Content.Server.Mapping;
@@ -26,6 +27,7 @@ using Content.Server.Worldgen.Tools;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Administration.Managers;
 using Content.Shared.Chat;
+using Content.Shared.FeedbackSystem;
 using Content.Shared.IoC;
 using Content.Shared.Kitchen;
 using Content.Shared.Players.PlayTimeTracking;
@@ -80,5 +82,7 @@ internal static class ServerContentIoC
         deps.Register<CVarControlManager>();
         deps.Register<DiscordLink>();
         deps.Register<DiscordChatLink>();
+        deps.Register<ServerFeedbackManager>();
+        deps.Register<ISharedFeedbackManager, ServerFeedbackManager>();
     }
 }
diff --git a/Content.Shared/CCVar/CCVars.Feedback.cs b/Content.Shared/CCVar/CCVars.Feedback.cs
new file mode 100644 (file)
index 0000000..90a3fc6
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+    /// <summary>
+    /// Used to set what popups are shown. Can accept multiple origins, just use spaces! See
+    /// <see cref="Content.Shared.FeedbackSystem.FeedbackPopupPrototype">FeedbackPopupPrototype</see>'s <see cref="Content.Shared.FeedbackSystem.FeedbackPopupPrototype.PopupOrigin">PopupOrigin</see> field.
+    /// Only prototypes who's PopupOrigin matches one of the FeedbackValidOrigins will be shown to players.
+    /// </summary>
+    /// <example>
+    /// wizden deltav
+    /// </example>
+    public static readonly CVarDef<string> FeedbackValidOrigins =
+        CVarDef.Create("feedback.valid_origins", "", CVar.SERVER | CVar.REPLICATED);
+}
diff --git a/Content.Shared/FeedbackSystem/FeedbackPopupMessage.cs b/Content.Shared/FeedbackSystem/FeedbackPopupMessage.cs
new file mode 100644 (file)
index 0000000..9b5d789
--- /dev/null
@@ -0,0 +1,61 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.FeedbackSystem;
+
+/// <summary>
+/// When clients receive this message a popup will appear with the contents from the given prototypes.
+/// </summary>
+public sealed class FeedbackPopupMessage : NetMessage
+{
+    public override MsgGroups MsgGroup => MsgGroups.Command;
+
+    /// <summary>
+    /// When true, the popup prototypes specified in this message will be removed from the client's list of feedback popups.
+    /// If no prototypes are specified, all popups will be removed.
+    /// </summary>
+    /// <remarks>If this is false and the list of prototypes is empty, the message will be ignored</remarks>
+    public bool Remove { get; set; }
+    public List<ProtoId<FeedbackPopupPrototype>>? FeedbackPrototypes;
+    public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+    {
+        Remove = buffer.ReadBoolean();
+        buffer.ReadPadBits();
+
+        var count = buffer.ReadVariableInt32();
+        FeedbackPrototypes = [];
+
+        for (var i = 0; i < count; i++)
+        {
+            FeedbackPrototypes.Add(new ProtoId<FeedbackPopupPrototype>(buffer.ReadString()));
+        }
+    }
+
+    public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+    {
+        buffer.Write(Remove);
+        buffer.WritePadBits();
+        buffer.WriteVariableInt32(FeedbackPrototypes?.Count ?? 0);
+
+        if (FeedbackPrototypes == null)
+            return;
+
+        foreach (var proto in FeedbackPrototypes)
+        {
+            buffer.Write(proto);
+        }
+    }
+}
+
+/// <summary>
+/// Sent from the server to open the feedback popup.
+/// </summary>
+public sealed class OpenFeedbackPopupMessage : NetMessage
+{
+    public override MsgGroups MsgGroup => MsgGroups.Command;
+    public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { }
+
+    public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { }
+}
diff --git a/Content.Shared/FeedbackSystem/FeedbackPopupPrototype.cs b/Content.Shared/FeedbackSystem/FeedbackPopupPrototype.cs
new file mode 100644 (file)
index 0000000..c7ff770
--- /dev/null
@@ -0,0 +1,56 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.FeedbackSystem;
+
+/// <summary>
+/// Prototype that describes the contents of a feedback popup.
+/// </summary>
+[Prototype]
+public sealed partial class FeedbackPopupPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; private set; } = null!;
+
+    /// <summary>
+    /// What server the popup is from, you must edit the ccvar to include this for the popup to appear!
+    /// </summary>
+    [DataField(required: true)]
+    public string PopupOrigin = string.Empty;
+
+    /// <summary>
+    /// Title of the popup. This supports rich text so you can use colors and stuff.
+    /// </summary>
+    [DataField(required: true)]
+    public string Title = string.Empty;
+
+    /// <summary>
+    /// List of "paragraphs" that are placed in the middle of the popup. Put any relevant information about what to give
+    /// feedback on here! [bold]Rich text is allowed[/bold]
+    /// </summary>
+    [DataField(required: true)]
+    public string Description = string.Empty;
+
+    /// <summary>
+    /// The kind of response the player should expect to give; good examples are "Survey", "Discord Channel", "Feedback Thread" etc.
+    /// Will be listed near the "Open Link" button; rich text is not allowed.
+    /// </summary>
+    [DataField]
+    public string? ResponseType;
+
+    /// <summary>
+    /// A link leading to where you want players to give feedback. Discord channel, form etc...
+    /// </summary>
+    [DataField]
+    public string? ResponseLink;
+
+    /// <summary>
+    /// Should this feedback be shown when the round ends.
+    /// </summary>
+    /// <remarks>
+    /// If this is false popups have to be shown to players by running the <pre>feedback:add</pre> command.<br />
+    /// This allows admins to show popups to only specific people.
+    /// </remarks>
+    [DataField]
+    public bool ShowRoundEnd = true;
+}
diff --git a/Content.Shared/FeedbackSystem/SharedFeedbackManager.Events.cs b/Content.Shared/FeedbackSystem/SharedFeedbackManager.Events.cs
new file mode 100644 (file)
index 0000000..24c3d1d
--- /dev/null
@@ -0,0 +1,21 @@
+using System.Linq;
+using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.FeedbackSystem;
+
+public abstract partial class SharedFeedbackManager : IEntityEventSubscriber
+{
+    [Dependency] private readonly IConfigurationManager _configManager = null!;
+
+    private void InitSubscriptions()
+    {
+       _configManager.OnValueChanged(CCVars.FeedbackValidOrigins, OnFeedbackOriginsUpdated, true);
+    }
+
+    private void OnFeedbackOriginsUpdated(string newOrigins)
+    {
+        _validOrigins = newOrigins.Split(' ').ToList();
+    }
+}
diff --git a/Content.Shared/FeedbackSystem/SharedFeedbackManager.cs b/Content.Shared/FeedbackSystem/SharedFeedbackManager.cs
new file mode 100644 (file)
index 0000000..e104be9
--- /dev/null
@@ -0,0 +1,150 @@
+using System.Linq;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.FeedbackSystem;
+
+/// <summary>
+/// SharedFeedbackManager handles feedback popup management and distribution across sessions.
+/// It manages the state of displayed popups and provides mechanisms for opening, displaying, removing,
+/// and sending popups to specified sessions or all sessions.
+/// </summary>
+public interface ISharedFeedbackManager
+{
+    /// <summary>
+    /// An event that is triggered whenever the set of displayed feedback popups changes.<br/>
+    /// The boolean parameter is true if new popups have been added
+    /// </summary>
+    event Action<bool>? DisplayedPopupsChanged;
+
+    /// <summary>
+    /// Adds the specified popup prototypes to the displayed popups on the client..
+    /// </summary>
+    /// <param name="prototypes">A list of popup prototype IDs to be added to the displayed prototypes</param>
+    /// <remarks>
+    /// This does nothing on the server.
+    /// <br/>
+    /// Use this if you want to add a popup from a shared or client-side entity system.
+    /// </remarks>
+    void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes) {}
+
+    /// <summary>
+    /// Removes the specified popup prototypes from the displayed popups on the client.
+    /// </summary>
+    /// <param name="prototypes">A list of popup prototype IDs to be removed from the displayed prototypes.
+    /// If null, all displayed popups will be cleared.</param>
+    /// <remarks>This does nothing on the server.</remarks>
+    void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes) {}
+
+    /// <summary>
+    /// Sends a list of feedback popup prototypes to a specific entity, identified by its EntityUid.
+    /// </summary>
+    /// <param name="uid">The unique identifier of the entity to send the feedback popups to.</param>
+    /// <param name="popupPrototypes">The list of feedback popup prototypes to send to the entity.</param>
+    /// <returns>Returns true if the feedback popups were successfully sent, otherwise false.</returns>
+    /// <remarks>This does nothing on the client.</remarks>
+    bool Send(EntityUid uid, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes)
+    {
+        return false;
+    }
+
+    /// <summary>
+    /// Sends a list of feedback popup prototypes to the specified session.
+    /// </summary>
+    /// <param name="session">The session to which the feedback popups will be sent.</param>
+    /// <param name="popupPrototypes">A list of feedback popup prototype IDs to send to the session.</param>
+    /// <param name="remove">When true, removes the specified prototypes instead of adding them</param>
+    /// <remarks>This does nothing on the client.</remarks>
+    void SendToSession(ICommonSession session, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false) {}
+
+    /// <summary>
+    /// Sends the specified feedback popup prototypes to all connected client sessions.
+    /// </summary>
+    /// <param name="popupPrototypes">A list of popup prototype IDs to be sent to all connected sessions.</param>
+    /// <param name="remove">When true, removes the specified prototypes instead of adding them</param>
+    /// <remarks>This does nothing on the client.</remarks>
+    void SendToAllSessions(List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false) {}
+
+    /// <summary>
+    /// Opens the feedback popup for a specific session.
+    /// </summary>
+    /// <param name="session">The session for which the feedback popup should be opened.</param>
+    /// <remarks>This does nothing on the client.</remarks>
+    void OpenForSession(ICommonSession session) {}
+
+    /// <summary>
+    /// Opens the feedback popup for all connected sessions.
+    /// </summary>
+    /// <remarks>This does nothing on the client.</remarks>
+    void OpenForAllSessions() {}
+}
+
+/// <inheritdoc cref="ISharedFeedbackManager" />
+public abstract partial class SharedFeedbackManager : ISharedFeedbackManager
+{
+    [Dependency] private readonly IPrototypeManager _proto = null!;
+    [Dependency] protected readonly INetManager NetManager = null!;
+
+    public virtual IReadOnlySet<ProtoId<FeedbackPopupPrototype>>? DisplayedPopups => null;
+
+    // <inheritdoc />
+    public event Action<bool>? DisplayedPopupsChanged;
+
+    /// <summary>
+    /// List of valid origns of the feedback popup that is filled from the CCVar. See
+    /// <see cref="Content.Shared.CCVar.CCVars.FeedbackValidOrigins">FeedbackValidOrigins</see>
+    /// </summary>
+    private List<string> _validOrigins = [];
+
+    [MustCallBase]
+    public virtual void Initialize()
+    {
+        InitSubscriptions();
+    }
+
+    /// <inheritdoc />
+    public virtual void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes) {}
+
+    /// <inheritdoc />
+    public virtual void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes) {}
+
+    /// <inheritdoc />
+    public virtual bool Send(EntityUid uid, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes)
+    {
+        return false;
+    }
+
+    /// <inheritdoc />
+    public virtual void SendToSession(ICommonSession session, List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false) {}
+
+    /// <inheritdoc />
+    public virtual void SendToAllSessions(List<ProtoId<FeedbackPopupPrototype>> popupPrototypes, bool remove = false) {}
+
+    /// <inheritdoc />
+    public virtual void OpenForSession(ICommonSession session) {}
+
+    /// <inheritdoc />
+    public virtual void OpenForAllSessions() {}
+
+    /// <summary>
+    /// Get a list of feedback prototypes that match the current valid origins.
+    /// </summary>
+    /// <param name="roundEndOnly">If true, only retrieve pop-ups with ShowRoundEnd set to true.</param>
+    /// <returns>Returns a list of protoIds; possibly empty.</returns>
+    public List<ProtoId<FeedbackPopupPrototype>> GetOriginFeedbackPrototypes(bool roundEndOnly)
+    {
+        var feedbackProtypes = _proto.EnumeratePrototypes<FeedbackPopupPrototype>()
+            .Where(x => (!roundEndOnly || x.ShowRoundEnd) && _validOrigins.Contains(x.PopupOrigin))
+            .Select(x => new ProtoId<FeedbackPopupPrototype>(x.ID))
+            .OrderBy(x => x.Id)
+            .ToList();
+
+        return feedbackProtypes;
+    }
+
+    protected void InvokeDisplayedPopupsChanged(bool show)
+    {
+        DisplayedPopupsChanged?.Invoke(show);
+    }
+}
index abf1179f90c3ad2d85de29a12c1227f60df9078e..f1f8661ac1965d69b4d6e531f5d3d8145a493e28 100644 (file)
@@ -7,4 +7,4 @@ ui-escape-guidebook = Guidebook
 ui-escape-wiki = Wiki
 ui-escape-disconnect = Disconnect
 ui-escape-quit = Quit
-
+ui-escape-feedback = Feedback
diff --git a/Resources/Locale/en-US/feedbackpopup/feedbackpopup.ftl b/Resources/Locale/en-US/feedbackpopup/feedbackpopup.ftl
new file mode 100644 (file)
index 0000000..4353bb6
--- /dev/null
@@ -0,0 +1,28 @@
+feedbackpopup-window-name = Request for feedback
+
+feedbackpopup-control-button-text = Open Link
+
+feedbackpopup-control-total-surveys = {$num ->
+    [one] {$num} entry
+   *[other] {$num} entries
+}
+feedbackpopup-control-no-entries= No entries
+feedbackpopup-control-ui-footer = Let us know what you think!
+
+# Command strings
+command-description-openfeedbackpopup = Opens the feedback popup window.
+command-description-feedback-show = Opens the feedback popup window for the given sessions.
+command-description-feedback-add = Adds a feedback popup prototype to the given clients and opens the popup window if the client didn't already have the prototype listed.
+command-description-feedback-remove = Removes a feedback popup prototype from the given clients.
+
+feedbackpopup-give-command-name = givefeedbackpopup
+feedbackpopup-show-command-name = showfeedbackpopup
+cmd-givefeedbackpopup-desc = Gives the targeted player a feedback popup.
+cmd-givefeedbackpopup-help = Usage: givefeedbackpopup <playerUid> <prototypeId>
+cmd-showfeedbackpopup-desc = Open the feedback popup window.
+cmd-showfeedbackpopup-help = Usage: showfeedbackpopup
+feedbackpopup-command-error-invalid-proto = Invalid feedback popup prototype.
+feedbackpopup-command-error-popup-send-fail = Failed to send popup! There probably isn't a mind attached to the given entity.
+feedbackpopup-command-success = Sent popup!
+feedbackpopup-command-hint-playerUid = <playerUid>
+feedbackpopup-command-hint-protoId = <prototypeId>
diff --git a/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml b/Resources/Prototypes/FeedbackPopup/feedbackpopups.yml
new file mode 100644 (file)
index 0000000..40cb149
--- /dev/null
@@ -0,0 +1,19 @@
+- type: feedbackPopup
+  id: FeedbackPopup
+  popupOrigin: wizden_master
+  title: "[bold]Feedback on the new feedback popup system[/bold]"
+  description: |-
+    This window you are seeing is a new system to get feedback on features. It will give popups at the end of the round (mostly on our testing server Vulture)!
+    Please share your thoughts through the forums below! Log in with your Space Station 14 account!
+  responseType: "Feedback Thread A"
+  responseLink: "https://forum.spacestation14.com/t/feedback-popup-feedback/22858"
+
+- type: feedbackPopup
+  id: GeneralFeedback
+  popupOrigin: wizden_master
+  title: "[bold]General feedback for the game[/bold]"
+  description: >-
+    If you have any feedback on the game, feel free to create a thread in the feedback forum category.
+  responseType: "General Feedback"
+  responseLink: "https://forum.spacestation14.com/c/development/feedback/51"
+  showRoundEnd: false