diff --git a/Content.Client/_DEN/PlayerRequest/PlayerRequestSystem.cs b/Content.Client/_DEN/PlayerRequest/PlayerRequestSystem.cs new file mode 100644 index 0000000000..f7497843da --- /dev/null +++ b/Content.Client/_DEN/PlayerRequest/PlayerRequestSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared._DEN.PlayerRequest.EntitySystems; + +namespace Content.Client._DEN.PlayerRequest; + +public sealed class PlayerRequestSystem : SharedPlayerRequestSystem; diff --git a/Content.Server/_DEN/PlayerRequest/PlayerRequestSystem.cs b/Content.Server/_DEN/PlayerRequest/PlayerRequestSystem.cs new file mode 100644 index 0000000000..00cd623b4c --- /dev/null +++ b/Content.Server/_DEN/PlayerRequest/PlayerRequestSystem.cs @@ -0,0 +1,75 @@ +using Content.Server.Popups; +using Content.Shared._DEN.PlayerRequest; +using Content.Shared._DEN.PlayerRequest.Components; +using Content.Shared._DEN.PlayerRequest.EntitySystems; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; + +namespace Content.Server._DEN.PlayerRequest; + +public sealed class PlayerRequestSystem : SharedPlayerRequestSystem +{ + [Dependency] private readonly PopupSystem _popupSystem = null!; + + public override void StartRequest(ProtoId request, EntityUid sender, EntityUid receiver) + { + if (!CanRequest(request, sender, receiver, out var reason)) + { + _popupSystem.PopupEntity(reason, sender, sender, PopupType.MediumCaution); + return; + } + + var ev = new AttemptPlayerRequestEvent(request, sender, receiver); + RaiseLocalEvent(ref ev); + + if (ev.Cancelled) + return; + + var senderComp = EnsureComp(sender); + var receiverComp = EnsureComp(receiver); + + senderComp.Receivers[request] = receiver; + receiverComp.Senders[request] = sender; + + Dirty(sender, senderComp); + Dirty(receiver, receiverComp); + + RaiseLocalEvent(new PlayerRequestStartedEvent(request, sender, receiver)); + } + + public override void ApproveRequest(ProtoId request, EntityUid receiver) + { + if (!TryComp(receiver, out var receiverComp) + || !TryGetSender(request, (receiver, receiverComp), out var sender)) + return; + + var statusUpdatedEvent = new PlayerRequestUpdatedEvent(request, receiver, sender, true); + RaiseLocalEvent(statusUpdatedEvent); + } + + public override void CancelRequest(ProtoId request, EntityUid cancelling) + { + Entity sender; + Entity receiver; + + if (TryGetSender(request, cancelling, out var potentialSender) + && TryComp(cancelling, out var receiverComp)) + { + sender = potentialSender; + receiver = (cancelling, receiverComp); + } + else if (TryGetReceiver(request, cancelling, out var potentialReceiver) + && TryComp(cancelling, out var senderComp)) + { + sender = (cancelling, senderComp); + receiver = potentialReceiver; + } + else + { + return; + } + + var statusUpdatedEvent = new PlayerRequestUpdatedEvent(request, receiver, sender, false); + RaiseLocalEvent(statusUpdatedEvent); + } +} diff --git a/Content.Shared/_DEN/PlayerRequest/Components/RequestReceiverComponent.cs b/Content.Shared/_DEN/PlayerRequest/Components/RequestReceiverComponent.cs new file mode 100644 index 0000000000..02e2396174 --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/Components/RequestReceiverComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.Components; + +/// +/// This is used for tracking active requests where the user is a receiver. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RequestReceiverComponent : Component +{ + /// + /// The other users involved in a player request. + /// + [DataField, AutoNetworkedField] + public Dictionary, EntityUid> Senders = new(); +} diff --git a/Content.Shared/_DEN/PlayerRequest/Components/RequestSenderComponent.cs b/Content.Shared/_DEN/PlayerRequest/Components/RequestSenderComponent.cs new file mode 100644 index 0000000000..8b6fcf2839 --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/Components/RequestSenderComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.Components; + +/// +/// This is used for tracking active requests where this user is the sender. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RequestSenderComponent : Component +{ + /// + /// The other user involved in a player request. + /// + [DataField, AutoNetworkedField] + public Dictionary, EntityUid> Receivers = new(); +} diff --git a/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.API.cs b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.API.cs new file mode 100644 index 0000000000..5f38b6e0ba --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.API.cs @@ -0,0 +1,133 @@ +using Content.Shared._DEN.PlayerRequest.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.EntitySystems; + +public abstract partial class SharedPlayerRequestSystem +{ + /// + /// Start a player request that sends the popups and alert as needed. + /// + /// The ID for this request. + /// The one who initiated the request. + /// The target of the request. + public virtual void StartRequest(ProtoId request, + EntityUid requester, + EntityUid target) + { + } + + /// + /// Cancels a player request, removing the alert and notifying the other party. + /// Sends a PlayerRequestStatusUpdated event. + /// + /// The ID for this request. + /// The user canceling the request. + public virtual void CancelRequest( + ProtoId request, + EntityUid cancelling) + { + } + + /// + /// Approves a player request, removing the alert. + /// also allows for an optional popup to the requester when approved. + /// Sends a PlayerRequestStatusUpdated event. + /// + /// The ID for this request. + /// The receiver who approved the request. + public virtual void ApproveRequest( + ProtoId request, + EntityUid receiver) + { + } + + /// + /// Tries to get the sender's from a receiver . + /// + /// The request that we should try to find the sender in. + /// The receiver that received the request. + /// The output sender that was acquired. + /// True if a valid EntityUid was found, false if not. + /// The entity will always exist if it returned true. + public bool TryGetSender(ProtoId request, + Entity receiver, + out Entity sender) + { + sender = EntityUid.Invalid; + + if (!Resolve(receiver, ref receiver.Comp, false) + || !receiver.Comp.Senders.TryGetValue(request, out var potentialSender) + || !Exists(potentialSender) || !TryComp(potentialSender, out var senderComp)) + return false; + + sender = (potentialSender, senderComp); + return true; + } + + /// + /// Tries to get the receiver's from a sender . + /// + /// The request that the sender sent. + /// The sender that sent the request. + /// The output receiver acquired. + /// True if a valid EntityUid was found, false if not. + /// The entity will always exist if it returned true. + public bool TryGetReceiver(ProtoId request, + Entity sender, + out Entity receiver) + { + receiver = EntityUid.Invalid; + + if (!Resolve(sender, ref sender.Comp, false) + || !sender.Comp.Receivers.TryGetValue(request, out var potentialReceiver) + || !Exists(potentialReceiver) || !TryComp(potentialReceiver, out var receiverComp)) + return false; + + receiver = (potentialReceiver, receiverComp); + return true; + } + + /// + /// Whether or not a request should go forth. + /// + /// The request attempting to be made. + /// The sender trying to send a request. + /// The receiver that would get the request. + /// The display-friendly message if we couldn't request. + /// Whether or not they can request in their current condition. + protected bool CanRequest( + ProtoId request, + EntityUid sender, + EntityUid receiver, + out string? reason) + { + reason = null; + + // If any of the users don't exist or don't have a mind, + // there's no one to send a request to. + if (!Exists(sender) || !Exists(receiver) + || !_mindSystem.TryGetMind(sender, out _, out _) + || !_mindSystem.TryGetMind(receiver, out _, out _)) + { + return false; + } + + // Alerts only support one alert at a time, so might as well enforce it. + if (TryComp(sender, out var requestSenderComp) + && requestSenderComp.Receivers.ContainsKey(request)) + { + reason = Loc.GetString("player-request-existing-sender", ("sender", sender)); + return false; + } + + if (TryComp(receiver, out var requestReceiverComp) + && requestReceiverComp.Senders.ContainsKey(request)) + { + reason = Loc.GetString("player-request-existing-receiver", ("receiver", receiver)); + return false; + } + + return true; + } +} diff --git a/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.Helpers.cs b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.Helpers.cs new file mode 100644 index 0000000000..084a72306f --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.Helpers.cs @@ -0,0 +1,37 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.EntitySystems; + +public abstract partial class SharedPlayerRequestSystem +{ + private void ShowAlert(ProtoId request, EntityUid target) + { + if (!Exists(target) || !TryComp(target, out var alerts)) + return; + + if (!_protoManager.TryIndex(request, out var requestProto)) + return; + + _alertsSystem.ShowAlert((target, alerts), requestProto.Alert); + } + + private void ClearAlert(ProtoId request, EntityUid target) + { + if (!Exists(target) || !TryComp(target, out var alerts)) + return; + + if (!_protoManager.TryIndex(request, out var requestProto)) + return; + + _alertsSystem.ClearAlert((target, alerts), requestProto.Alert); + } + + private void InitializePrototypes() + { + foreach (var requestPrototype in _protoManager.EnumeratePrototypes()) + { + _alertTranslations[requestPrototype.Alert] = requestPrototype; + } + } +} diff --git a/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.cs b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.cs new file mode 100644 index 0000000000..a021558225 --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/EntitySystems/SharedPlayerRequestSystem.cs @@ -0,0 +1,119 @@ +using Content.Shared._DEN.PlayerRequest.Components; +using Content.Shared._DEN.PlayerRequest.Events; +using Content.Shared.Alert; +using Content.Shared.Mind; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.EntitySystems; + +/// +/// Handles player-to-player requests, such as when a player offers something. +/// +public abstract partial class SharedPlayerRequestSystem : EntitySystem +{ + [Dependency] private readonly AlertsSystem _alertsSystem = null!; + [Dependency] private readonly SharedMindSystem _mindSystem = null!; + [Dependency] private readonly SharedPopupSystem _popupSystem = null!; + [Dependency] private readonly IPrototypeManager _protoManager = null!; + + private const PopupType RequestPopupType = PopupType.Medium; + private readonly Dictionary, ProtoId> _alertTranslations = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRequestAcceptedAlert); + SubscribeLocalEvent(OnRequestStarted); + SubscribeLocalEvent(OnRequestUpdated); + + InitializePrototypes(); + _protoManager.PrototypesReloaded += OnPrototypesReloaded; + } + + private void OnRequestStarted(PlayerRequestStartedEvent ev) + { + if (!_protoManager.TryIndex(ev.RequestId, out var playerRequest)) + return; + + var receivePopup = Loc.GetString(playerRequest.ReceivePopup, ("sender", ev.Sender)); + var sendPopup = Loc.GetString(playerRequest.SendPopup, ("receiver", ev.Receiver)); + + ShowAlert(ev.RequestId, ev.Receiver); + + _popupSystem.PopupPredicted(receivePopup, ev.Receiver, ev.Receiver, RequestPopupType); + _popupSystem.PopupPredicted(sendPopup, ev.Sender, ev.Sender, RequestPopupType); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (args.WasModified()) + InitializePrototypes(); + } + + private void OnRequestAcceptedAlert(AcceptedRequestAlertEvent ev) + { + if (!_alertTranslations.TryGetValue(ev.AlertId, out var requestId)) + return; + + if (!TryGetSender(requestId, ev.User, out var sender)) + return; + + var statusUpdatedEvent = new PlayerRequestUpdatedEvent(requestId, ev.User, sender, true); + RaiseLocalEvent(statusUpdatedEvent); + } + + private void OnRequestUpdated(PlayerRequestUpdatedEvent ev) + { + if (ev.IsApproved) + OnRequestApproved(ev); + + if (!TryComp(ev.Receiver, out var receiverComp) + || !TryComp(ev.Sender, out var senderComp)) + return; + + receiverComp.Senders.Remove(ev.RequestId); + senderComp.Receivers.Remove(ev.RequestId); + + Dirty(ev.Receiver, receiverComp); + Dirty(ev.Sender, senderComp); + } + + private void OnRequestApproved(PlayerRequestUpdatedEvent ev) + { + var requestProto = _protoManager.Index(ev.RequestId); + + if (!TryComp(ev.Receiver, out var requestComp)) + return; + + if (!TryGetSender(ev.RequestId, (ev.Receiver, requestComp), out var sender)) + return; + + ClearAlert(ev.RequestId, ev.Receiver); + + if (requestProto.AcceptPopup == null) + return; + + var acceptPopup = Loc.GetString(requestProto.AcceptPopup, ("receiver", ev.Receiver)); + _popupSystem.PopupPredicted(acceptPopup, sender, sender, RequestPopupType); + } +} + +[ByRefEvent] +public record struct AttemptPlayerRequestEvent( + ProtoId RequestId, + EntityUid Sender, + EntityUid Receiver, + bool Cancelled = false); + +public record struct PlayerRequestStartedEvent( + ProtoId RequestId, + EntityUid Sender, + EntityUid Receiver); + +public record struct PlayerRequestUpdatedEvent( + ProtoId RequestId, + EntityUid Receiver, + EntityUid Sender, + bool IsApproved); diff --git a/Content.Shared/_DEN/PlayerRequest/Events/PlayerRequestEvents.cs b/Content.Shared/_DEN/PlayerRequest/Events/PlayerRequestEvents.cs new file mode 100644 index 0000000000..8f0bf3bddd --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/Events/PlayerRequestEvents.cs @@ -0,0 +1,23 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.PlayerRequest.Events; + +[ByRefEvent] +public record struct AttemptPlayerRequestEvent( + ProtoId RequestId, + EntityUid Sender, + EntityUid Receiver, + bool Cancelled = false); + +public record struct PlayerRequestStartedEvent( + ProtoId RequestId, + EntityUid Sender, + EntityUid Receiver); + +public record struct PlayerRequestUpdatedEvent( + ProtoId RequestId, + EntityUid Receiver, + bool IsApproved); + +public sealed partial class AcceptedRequestAlertEvent : BaseAlertEvent; diff --git a/Content.Shared/_DEN/PlayerRequest/Prototypes/PlayerRequestPrototype.cs b/Content.Shared/_DEN/PlayerRequest/Prototypes/PlayerRequestPrototype.cs new file mode 100644 index 0000000000..fb61d32c8c --- /dev/null +++ b/Content.Shared/_DEN/PlayerRequest/Prototypes/PlayerRequestPrototype.cs @@ -0,0 +1,61 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared._DEN.PlayerRequest; + +/// +/// This is a prototype for declaring player requests +/// +[Prototype] +public sealed class PlayerRequestPrototype : IPrototype, IInheritingPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; } + + /// + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; } + + /// + /// The specific alert to use for this request. + /// + [DataField] + public ProtoId Alert = "PlayerRequestAcceptAlert"; + + /// + /// How long (in seconds) should the request stay waiting for a player to accept before canceling? + /// + [DataField] + public int AutoDeclineAfter { get; } = 30; + + /// + /// The popup message that will appear when a player receives this request. + /// + [DataField(required: true)] + public LocId ReceivePopup { get; } + + /// + /// The popup message that will appear when a player sends the request. + /// + [DataField(required: true)] + public LocId SendPopup { get; } + + /// + /// The popup message that will appear to the requester when the target player accepts this request. + /// + [DataField] + public LocId? AcceptPopup { get; } + + /// + /// The popup message that will appear to the requester when the target player accepts this request. + /// + [DataField] + public LocId? CancelledPopup { get; } +}