Skip to content

Commit

Permalink
Feedback popups (#2561)
Browse files Browse the repository at this point in the history
* First commit

* Added webhook

* Added round number support

* More fixes

* Fixes

* Merge conflict begone

* how is that even possible
  • Loading branch information
beck-thompson authored Jan 2, 2025
1 parent 05cd49d commit 36a18de
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 0 deletions.
39 changes: 39 additions & 0 deletions Content.Client/_DV/FeedbackPopup/FeedbackPopupUIController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Content.Shared._DV.FeedbackOverwatch;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Network;

namespace Content.Client._DV.FeedbackPopup;

/// <summary>
/// This handles getting feedback popup messages from the server and making a popup in the client.
/// Currently, this system can only support one window at a time.
/// </summary>
public sealed class FeedbackPopupUIController : UIController
{
[Dependency] private readonly IClientNetManager _net = default!;

private FeedbackPopupWindow? _window;

public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<FeedbackPopupMessage>(OnFeedbackPopup);
}

private void OnFeedbackPopup(FeedbackPopupMessage msg, EntitySessionEventArgs args)
{
// If a window is already open, close it
_window?.Close();

_window = new FeedbackPopupWindow(msg.FeedbackPrototype);
_window.OpenCentered();
_window.OnClose += () => _window = null;
_window.OnSubmitted += OnFeedbackSubmitted;
}

private void OnFeedbackSubmitted((LocId, string) args)
{
_net.ClientSendMessage(new FeedbackResponseMessage{ FeedbackName = Loc.GetString(args.Item1), FeedbackMessage = args.Item2 });
_window?.Close();
}
}
15 changes: 15 additions & 0 deletions Content.Client/_DV/FeedbackPopup/FeedbackPopupWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'feedbackpopup-window-name'}"
MinSize="300 250"
SetSize="550 400">
<BoxContainer Orientation="Vertical" Margin="10 0 20 0">
<RichTextLabel HorizontalAlignment="Center" Name="TitleLabel" Margin="0 0 0 20"></RichTextLabel>
<BoxContainer Name="SectionContainer" Orientation="Vertical"></BoxContainer>
<BoxContainer Orientation="Vertical" Name="FeedbackReplyContainer" Visible="False">
<TextEdit Name="FeedbackReply" MaxHeight="200" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" MinHeight="100" />
<Button Name="SubmitButton" Text="{Loc 'feedbackpopup-submit-feedback-button'}"></Button>
<Label HorizontalAlignment="Center" Text="{Loc 'feedbackpopup-disclaimer'}" StyleClasses="LabelSmall"></Label>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
57 changes: 57 additions & 0 deletions Content.Client/_DV/FeedbackPopup/FeedbackPopupWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.FeedbackOverwatch;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Client._DV.FeedbackPopup;

[GenerateTypedNameReferences]
public sealed partial class FeedbackPopupWindow : FancyWindow
{
[Dependency] private readonly IPrototypeManager _proto = default!;

private readonly FeedbackPopupPrototype _feedbackpopup;

public event Action<(LocId, string)>? OnSubmitted;

public FeedbackPopupWindow(ProtoId<FeedbackPopupPrototype> popupProto)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

// Save the proto so we can use it later.
_feedbackpopup = _proto.Index(popupProto);

// When the submit button is pressed, pass up the vars back to the UI controller.
SubmitButton.OnPressed += _ => OnSubmitted?.Invoke((_feedbackpopup.PopupName, Rope.Collapse(FeedbackReply.TextRope)));

PopulateWindow();
}

private void PopulateWindow()
{
// Title
TitleLabel.Text = Loc.GetString(_feedbackpopup.Title);

// Description
foreach (var section in _feedbackpopup.Description)
CreateSection(Loc.GetString(section));

// Set the feedback submission to the correct visibility
FeedbackReplyContainer.Visible = _feedbackpopup.FeedbackField;
}

private void CreateSection(string text)
{
var label = new RichTextLabel
{
Text = text,
Margin = new Thickness(0,0,0,10),
};
SectionContainer.AddChild(label);
}
}

4 changes: 4 additions & 0 deletions Content.Server/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server._DV.FeedbackPopup; // DeltaV
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Server.GhostKick;
Expand Down Expand Up @@ -47,6 +48,7 @@ public sealed class EntryPoint : GameServer
private PlayTimeTrackingManager? _playTimeTracking;
private IEntitySystemManager? _sysMan;
private IServerDbManager? _dbManager;
private FeedbackPopupManager? _feedbackPopupManager; // DeltaV

/// <inheritdoc />
public override void Init()
Expand Down Expand Up @@ -93,6 +95,7 @@ public override void Init()
_playTimeTracking = IoCManager.Resolve<PlayTimeTrackingManager>();
_sysMan = IoCManager.Resolve<IEntitySystemManager>();
_dbManager = IoCManager.Resolve<IServerDbManager>();
_feedbackPopupManager = IoCManager.Resolve<FeedbackPopupManager>(); // DeltaV

logManager.GetSawmill("Storage").Level = LogLevel.Info;
logManager.GetSawmill("db.ef").Level = LogLevel.Info;
Expand All @@ -110,6 +113,7 @@ public override void Init()
_voteManager.Initialize();
_updateManager.Initialize();
_playTimeTracking.Initialize();
_feedbackPopupManager.Initialize(); // DeltaV
IoCManager.Resolve<JobWhitelistManager>().Initialize();
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
}
Expand Down
2 changes: 2 additions & 0 deletions Content.Server/IoC/ServerContentIoC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server._DV.FeedbackPopup; // DeltaV
using Content.Server.Discord;
using Content.Server.Discord.WebhookMessages;
using Content.Server.EUI;
Expand Down Expand Up @@ -73,6 +74,7 @@ public static void Register()
IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
IoCManager.Register<FeedbackPopupManager>(); // DeltaV
}
}
}
82 changes: 82 additions & 0 deletions Content.Server/_DV/FeedbackPopup/FeedbackPopupManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Content.Server.Discord;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.FeedbackOverwatch;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Content.Shared.GameTicking;

namespace Content.Server._DV.FeedbackPopup;

/// <summary>
/// This manager sends feedback from players to the discord through a webhook.
/// </summary>
public sealed class FeedbackPopupManager
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IEntityManager _entity = default!;

private SharedGameTicker? _ticker;

private ISawmill _sawmill = default!;

/// <summary>
/// Webhook to send the messages to!
/// </summary>
private string _webhookUrl = default!;

public void Initialize()
{
_sawmill = Logger.GetSawmill("feedback");

_netManager.RegisterNetMessage<FeedbackResponseMessage>(RecieveFeedbackResponse);
_cfg.OnValueChanged(DCCVars.DiscordPlayerFeedbackWebhook, SetWebhookUrl, true);
}

private void SetWebhookUrl(string webhookUrl)
{
_webhookUrl = webhookUrl;
}

private void RecieveFeedbackResponse(FeedbackResponseMessage message)
{
// Post inject doesn't work for the entity manager (Entity manager is only initialized after this system is initialized)
_ticker ??= _entity.System<SharedGameTicker>();

if (string.IsNullOrWhiteSpace(_webhookUrl))
return;

SendDiscordWebhookMessage(CreateMessage(message.FeedbackName, message.FeedbackMessage, _ticker.RoundId, message.MsgChannel.UserName));
}

private string CreateMessage(string feedbackName, string feedback, int roundNumber, string username)
{
var header = Loc.GetString("feedbackpopup-discord-format-header", ("roundNumber", roundNumber), ("playerName", username));
var info = Loc.GetString("feedbackpopup-discord-format-info", ("feedbackName", feedbackName));
var spacer = Loc.GetString("feedbackpopup-discord-format-spacer");
var feedbackbody = Loc.GetString("feedbackpopup-discord-format-feedbackbody", ("feedback", feedback));

return header + info + "\n" + spacer + "\n" + feedbackbody;
}

private async void SendDiscordWebhookMessage(string msg)
{
try
{
var webhookData = await _discord.GetWebhook(_webhookUrl);
if (webhookData == null)
return;

var webhookIdentifier = webhookData.Value.ToIdentifier();

var payload = new WebhookPayload { Content = msg };

await _discord.CreateMessage(webhookIdentifier, payload);
}
catch (Exception e)
{
_sawmill.Error($"Error while sending discord watchlist connection message:\n{e}");
}
}
}
10 changes: 10 additions & 0 deletions Content.Shared/_DV/CCVars/DCCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,14 @@ public sealed class DCCVars
/// </summary>
public static readonly CVarDef<bool> Shipyard =
CVarDef.Create("shuttle.shipyard", true, CVar.SERVERONLY);

/*
* Feedback webhook
*/

/// <summary>
/// Discord webhook URL for getting feedback from players. If empty, will not relay the feedback.
/// </summary>
public static readonly CVarDef<string> DiscordPlayerFeedbackWebhook =
CVarDef.Create("discord.player_feedback_webhook", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
}
52 changes: 52 additions & 0 deletions Content.Shared/_DV/FeedbackOverwatch/FeedbackOverwatchEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

namespace Content.Shared._DV.FeedbackOverwatch;

/// <summary>
/// When clients recieve this message a popup will appear on their screen with the contents from the given prototype.
/// </summary>
[Serializable, NetSerializable]
public sealed class FeedbackPopupMessage : EntityEventArgs
{
public ProtoId<FeedbackPopupPrototype> FeedbackPrototype;

public FeedbackPopupMessage(ProtoId<FeedbackPopupPrototype> feedbackPrototype)
{
FeedbackPrototype = feedbackPrototype;
}
}

/// <summary>
/// Stores a users response to feedback.
/// </summary>
public sealed class FeedbackResponseMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.EntityEvent;

/// <summary>
/// The feedback that the user is sending.
/// </summary>
public string FeedbackName = string.Empty;

/// <summary>
/// The feedback that the user is sending.
/// </summary>
public string FeedbackMessage = string.Empty;

public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
FeedbackName = buffer.ReadString();
FeedbackMessage = buffer.ReadString();
}

public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(FeedbackName);
buffer.Write(FeedbackMessage);
}

public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
}
41 changes: 41 additions & 0 deletions Content.Shared/_DV/FeedbackOverwatch/FeedbackPopupPrototype.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Robust.Shared.Prototypes;

namespace Content.Shared._DV.FeedbackOverwatch;

/// <summary>
/// Prototype that describes the contents of a feedback popup.
/// </summary>
[Prototype]
public sealed partial class FeedbackPopupPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;

/// <summary>
/// Name of the popup. This is relayed in the discord webhook.
/// </summary>
/// <remarks>
/// Recommended to keep this one word to make searching easier.
/// </remarks>
[DataField(required: true)]
public LocId PopupName;

/// <summary>
/// Title of the popup. This supports rich text so you can use colors and stuff.
/// </summary>
[DataField(required: true)]
public LocId Title;

/// <summary>
/// List of "paragraphs" that are placed in the middle of the popup. Put any relevant information about what to give feedback on here!
/// </summary>
[DataField(required: true)]
public List<LocId> Description = new();

/// <summary>
/// If true, will show a text field that players can fill out and will be piped through the discord webhook if enabled.
/// </summary>
[DataField]
public bool FeedbackField = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Content.Shared._DV.FeedbackOverwatch;

public sealed partial class SharedFeedbackOverwatchSystem
{
private void InitializeEvents()
{
// Subscribe to events that would be good for popups here. If it's a DeltaV specific system, do the subscriptions
// in there (And import the SharedFeedbackOverwatchSystem to use SendPopup). If it's an upstream system try to
// add the subscription here to avoid merge conflicts.
}
}
Loading

0 comments on commit 36a18de

Please sign in to comment.