]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Adds a refund button & action upgrades for stores (#24518)
authorkeronshb <54602815+keronshb@users.noreply.github.com>
Sun, 4 Feb 2024 00:48:51 +0000 (19:48 -0500)
committerGitHub <noreply@github.com>
Sun, 4 Feb 2024 00:48:51 +0000 (11:48 +1100)
* adds refunds to stores

* Adds method to check for starting map

* comments, datafields, some requested changes

* turns event into ref event

* Adds datafields

* Switches to entity terminating event

* Changes store entity to be nullable and checks if store is terminating to remove reference.

* Tryadd instead of containskey

* Adds a refund disable method, disables refund on bought ent container changes if not an action

* Removes datafield specification

* Readds missing using statement

* Removes unused using statements

* What the heck is listing data

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Content.Client/Store/Ui/StoreBoundUserInterface.cs
Content.Client/Store/Ui/StoreMenu.xaml
Content.Client/Store/Ui/StoreMenu.xaml.cs
Content.Server/Store/Components/StoreComponent.cs
Content.Server/Store/StoreRefundComponent.cs [new file with mode: 0644]
Content.Server/Store/Systems/StoreSystem.Refund.cs [new file with mode: 0644]
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Server/Store/Systems/StoreSystem.cs
Content.Shared/Store/ListingPrototype.cs
Content.Shared/Store/StoreUi.cs

index 6774ef35a021fb1b947a0e5dcb47a95fa33f9793..b549918d7c41b9fb7baa7f2b695d81c342b59d86 100644 (file)
@@ -48,6 +48,11 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
         {
             SendMessage(new StoreRequestUpdateInterfaceMessage());
         };
+
+        _menu.OnRefundAttempt += (_) =>
+        {
+            SendMessage(new StoreRequestRefundMessage());
+        };
     }
     protected override void UpdateState(BoundUserInterfaceState state)
     {
@@ -64,6 +69,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
 
                 _menu.UpdateListing(msg.Listings.ToList());
                 _menu.SetFooterVisibility(msg.ShowFooter);
+                _menu.UpdateRefund(msg.AllowRefund);
                 break;
             case StoreInitializeState msg:
                 _windowName = msg.Name;
index a454e3e2b7f4feaa70996ed795e5bdad6166d823..4b38352a44adffd1d3582632c1b2cc28caef5920 100644 (file)
                     MinWidth="64"
                     HorizontalAlignment="Right"
                     Text="{Loc 'store-ui-default-withdraw-text'}" />
+                <Button
+                    Name="RefundButton"
+                    MinWidth="64"
+                    HorizontalAlignment="Right"
+                    Text="Refund" />
             </BoxContainer>
             <PanelContainer VerticalExpand="True">
                 <PanelContainer.PanelOverride>
index d938dbfe542c64642ef9f81ce109765b0142c82e..5dc1ab246bd19208ee78c394d0bcafcbececfaa3 100644 (file)
@@ -31,6 +31,7 @@ public sealed partial class StoreMenu : DefaultWindow
     public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
     public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
     public event Action<BaseButton.ButtonEventArgs>? OnRefreshButtonPressed;
+    public event Action<BaseButton.ButtonEventArgs>? OnRefundAttempt;
 
     public Dictionary<string, FixedPoint2> Balance = new();
     public string CurrentCategory = string.Empty;
@@ -44,6 +45,8 @@ public sealed partial class StoreMenu : DefaultWindow
 
         WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
         RefreshButton.OnButtonDown += OnRefreshButtonDown;
+        RefundButton.OnButtonDown += OnRefundButtonDown;
+
         if (Window != null)
             Window.Title = name;
     }
@@ -116,6 +119,11 @@ public sealed partial class StoreMenu : DefaultWindow
         _withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
     }
 
+    private void OnRefundButtonDown(BaseButton.ButtonEventArgs args)
+    {
+        OnRefundAttempt?.Invoke(args);
+    }
+
     private void AddListingGui(ListingData listing)
     {
         if (!listing.Categories.Contains(CurrentCategory))
@@ -262,6 +270,11 @@ public sealed partial class StoreMenu : DefaultWindow
         _withdrawWindow?.Close();
     }
 
+    public void UpdateRefund(bool allowRefund)
+    {
+        RefundButton.Disabled = !allowRefund;
+    }
+
     private sealed class StoreCategoryButton : Button
     {
         public string? Id;
index 54f8c9ee15b2fddfe4564db4e8840b4adf18b5df..063e25fbf9fec8dcc3f35b307cde81b872c9f9b2 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.FixedPoint;
 using Content.Shared.Store;
 using Robust.Shared.Audio;
+using Robust.Shared.Map;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
@@ -59,6 +60,30 @@ public sealed partial class StoreComponent : Component
     [ViewVariables]
     public HashSet<ListingData> LastAvailableListings = new();
 
+    /// <summary>
+    ///     All current entities bought from this shop. Useful for keeping track of refunds and upgrades.
+    /// </summary>
+    [ViewVariables, DataField]
+    public List<EntityUid> BoughtEntities = new();
+
+    /// <summary>
+    ///     The total balance spent in this store. Used for refunds.
+    /// </summary>
+    [ViewVariables, DataField]
+    public Dictionary<string, FixedPoint2> BalanceSpent = new();
+
+    /// <summary>
+    ///     Controls if the store allows refunds
+    /// </summary>
+    [ViewVariables, DataField]
+    public bool RefundAllowed;
+
+    /// <summary>
+    ///     The map the store was originally from, used to block refunds if the map is changed
+    /// </summary>
+    [DataField]
+    public EntityUid? StartingMap;
+
     #region audio
     /// <summary>
     /// The sound played to the buyer when a purchase is succesfully made.
@@ -78,3 +103,17 @@ public readonly record struct StoreAddedEvent;
 /// </summary>
 [ByRefEvent]
 public readonly record struct StoreRemovedEvent;
+
+/// <summary>
+///     Broadcast when an Entity with the <see cref="StoreRefundComponent"/> is deleted
+/// </summary>
+[ByRefEvent]
+public readonly struct RefundEntityDeletedEvent
+{
+    public EntityUid Uid { get; }
+
+    public RefundEntityDeletedEvent(EntityUid uid)
+    {
+        Uid = uid;
+    }
+}
diff --git a/Content.Server/Store/StoreRefundComponent.cs b/Content.Server/Store/StoreRefundComponent.cs
new file mode 100644 (file)
index 0000000..1a6b17c
--- /dev/null
@@ -0,0 +1,13 @@
+using Content.Server.Store.Systems;
+
+namespace Content.Server.Store.Components;
+
+/// <summary>
+///     Keeps track of entities bought from stores for refunds, especially useful if entities get deleted before they can be refunded.
+/// </summary>
+[RegisterComponent, Access(typeof(StoreSystem))]
+public sealed partial class StoreRefundComponent : Component
+{
+    [ViewVariables, DataField]
+    public EntityUid? StoreEntity;
+}
diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs
new file mode 100644 (file)
index 0000000..c42c794
--- /dev/null
@@ -0,0 +1,56 @@
+using Content.Server.Actions;
+using Content.Server.Store.Components;
+using Content.Shared.Actions;
+using Robust.Shared.Containers;
+
+namespace Content.Server.Store.Systems;
+
+public sealed partial class StoreSystem
+{
+    private void InitializeRefund()
+    {
+        SubscribeLocalEvent<StoreComponent, EntityTerminatingEvent>(OnStoreTerminating);
+        SubscribeLocalEvent<StoreRefundComponent, EntityTerminatingEvent>(OnRefundTerminating);
+        SubscribeLocalEvent<StoreRefundComponent, EntRemovedFromContainerMessage>(OnEntityRemoved);
+        SubscribeLocalEvent<StoreRefundComponent, EntInsertedIntoContainerMessage>(OnEntityInserted);
+    }
+
+    private void OnEntityRemoved(EntityUid uid, StoreRefundComponent component, EntRemovedFromContainerMessage args)
+    {
+        if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _) || !TryComp<StoreComponent>(component.StoreEntity.Value, out var storeComp))
+            return;
+
+        DisableRefund(component.StoreEntity.Value, storeComp);
+    }
+
+    private void OnEntityInserted(EntityUid uid, StoreRefundComponent component, EntInsertedIntoContainerMessage args)
+    {
+        if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _) || !TryComp<StoreComponent>(component.StoreEntity.Value, out var storeComp))
+            return;
+
+        DisableRefund(component.StoreEntity.Value, storeComp);
+    }
+
+    private void OnStoreTerminating(Entity<StoreComponent> ent, ref EntityTerminatingEvent args)
+    {
+        if (ent.Comp.BoughtEntities.Count <= 0)
+            return;
+
+        foreach (var boughtEnt in ent.Comp.BoughtEntities)
+        {
+            if (!TryComp<StoreRefundComponent>(boughtEnt, out var refundComp))
+                continue;
+
+            refundComp.StoreEntity = null;
+        }
+    }
+
+    private void OnRefundTerminating(Entity<StoreRefundComponent> ent, ref EntityTerminatingEvent args)
+    {
+        if (ent.Comp.StoreEntity == null)
+            return;
+
+        var ev = new RefundEntityDeletedEvent(ent);
+        RaiseLocalEvent(ent.Comp.StoreEntity.Value, ref ev);
+    }
+}
index 7599b08b3ce4049cb3bbdf52cdfd838e95966be7..a7490fd27ff99cd7bd7bc0105fc214308467351d 100644 (file)
@@ -4,11 +4,13 @@ using Content.Server.Administration.Logs;
 using Content.Server.PDA.Ringer;
 using Content.Server.Stack;
 using Content.Server.Store.Components;
-using Content.Shared.UserInterface;
+using Content.Shared.Actions;
 using Content.Shared.Database;
 using Content.Shared.FixedPoint;
 using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Mind;
 using Content.Shared.Store;
+using Content.Shared.UserInterface;
 using Robust.Server.GameObjects;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Player;
@@ -20,6 +22,9 @@ public sealed partial class StoreSystem
     [Dependency] private readonly IAdminLogManager _admin = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly ActionsSystem _actions = default!;
+    [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+    [Dependency] private readonly ActionUpgradeSystem _actionUpgrade = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly StackSystem _stack = default!;
     [Dependency] private readonly UserInterfaceSystem _ui = default!;
@@ -29,6 +34,13 @@ public sealed partial class StoreSystem
         SubscribeLocalEvent<StoreComponent, StoreRequestUpdateInterfaceMessage>(OnRequestUpdate);
         SubscribeLocalEvent<StoreComponent, StoreBuyListingMessage>(OnBuyRequest);
         SubscribeLocalEvent<StoreComponent, StoreRequestWithdrawMessage>(OnRequestWithdraw);
+        SubscribeLocalEvent<StoreComponent, StoreRequestRefundMessage>(OnRequestRefund);
+        SubscribeLocalEvent<StoreComponent, RefundEntityDeletedEvent>(OnRefundEntityDeleted);
+    }
+
+    private void OnRefundEntityDeleted(Entity<StoreComponent> ent, ref RefundEntityDeletedEvent args)
+    {
+        ent.Comp.BoughtEntities.Remove(args.Uid);
     }
 
     /// <summary>
@@ -98,7 +110,7 @@ public sealed partial class StoreSystem
 
         // only tell operatives to lock their uplink if it can be locked
         var showFooter = HasComp<RingerUplinkComponent>(store);
-        var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter);
+        var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed);
         _ui.SetUiState(ui, state);
     }
 
@@ -118,6 +130,7 @@ public sealed partial class StoreSystem
     private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListingMessage msg)
     {
         var listing = component.Listings.FirstOrDefault(x => x.Equals(msg.Listing));
+
         if (listing == null) //make sure this listing actually exists
         {
             Log.Debug("listing does not exist");
@@ -149,10 +162,20 @@ public sealed partial class StoreSystem
                 return;
             }
         }
+
+        if (!IsOnStartingMap(uid, component))
+            component.RefundAllowed = false;
+        else
+            component.RefundAllowed = true;
+
         //subtract the cash
-        foreach (var currency in listing.Cost)
+        foreach (var (currency, value) in listing.Cost)
         {
-            component.Balance[currency.Key] -= currency.Value;
+            component.Balance[currency] -= value;
+
+            component.BalanceSpent.TryAdd(currency, FixedPoint2.Zero);
+
+            component.BalanceSpent[currency] += value;
         }
 
         //spawn entity
@@ -160,13 +183,71 @@ public sealed partial class StoreSystem
         {
             var product = Spawn(listing.ProductEntity, Transform(buyer).Coordinates);
             _hands.PickupOrDrop(buyer, product);
+
+            HandleRefundComp(uid, component, product);
+
+            var xForm = Transform(product);
+
+            if (xForm.ChildCount > 0)
+            {
+                var childEnumerator = xForm.ChildEnumerator;
+                while (childEnumerator.MoveNext(out var child))
+                {
+                    component.BoughtEntities.Add(child);
+                }
+            }
         }
 
         //give action
         if (!string.IsNullOrWhiteSpace(listing.ProductAction))
         {
+            EntityUid? actionId;
             // I guess we just allow duplicate actions?
-            _actions.AddAction(buyer, listing.ProductAction);
+            // Allow duplicate actions and just have a single list buy for the buy-once ones.
+            if (!_mind.TryGetMind(buyer, out var mind, out _))
+                actionId = _actions.AddAction(buyer, listing.ProductAction);
+            else
+                actionId = _actionContainer.AddAction(mind, listing.ProductAction);
+
+            // Add the newly bought action entity to the list of bought entities
+            // And then add that action entity to the relevant product upgrade listing, if applicable
+            if (actionId != null)
+            {
+                HandleRefundComp(uid, component, actionId.Value);
+
+                if (listing.ProductUpgradeID != null)
+                {
+                    foreach (var upgradeListing in component.Listings)
+                    {
+                        if (upgradeListing.ID == listing.ProductUpgradeID)
+                        {
+                            upgradeListing.ProductActionEntity = actionId.Value;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (listing is { ProductUpgradeID: not null, ProductActionEntity: not null })
+        {
+            if (listing.ProductActionEntity != null)
+            {
+                component.BoughtEntities.Remove(listing.ProductActionEntity.Value);
+            }
+
+            if (!_actionUpgrade.TryUpgradeAction(listing.ProductActionEntity, out var upgradeActionId))
+            {
+                if (listing.ProductActionEntity != null)
+                    HandleRefundComp(uid, component, listing.ProductActionEntity.Value);
+
+                return;
+            }
+
+            listing.ProductActionEntity = upgradeActionId;
+
+            if (upgradeActionId != null)
+                HandleRefundComp(uid, component, upgradeActionId.Value);
         }
 
         //broadcast event
@@ -225,4 +306,71 @@ public sealed partial class StoreSystem
         component.Balance[msg.Currency] -= msg.Amount;
         UpdateUserInterface(buyer, uid, component);
     }
+
+    private void OnRequestRefund(EntityUid uid, StoreComponent component, StoreRequestRefundMessage args)
+    {
+        // TODO: Remove guardian/holopara
+
+        if (args.Session.AttachedEntity is not { Valid: true } buyer)
+            return;
+
+        if (!IsOnStartingMap(uid, component))
+        {
+            component.RefundAllowed = false;
+            UpdateUserInterface(buyer, uid, component);
+        }
+
+        if (!component.RefundAllowed || component.BoughtEntities.Count == 0)
+            return;
+
+        for (var i = component.BoughtEntities.Count; i >= 0; i--)
+        {
+            var purchase = component.BoughtEntities[i];
+
+            if (!Exists(purchase))
+                continue;
+
+            component.BoughtEntities.RemoveAt(i);
+
+            if (_actions.TryGetActionData(purchase, out var actionComponent))
+            {
+                _actionContainer.RemoveAction(purchase, actionComponent);
+            }
+
+            EntityManager.DeleteEntity(purchase);
+        }
+
+        foreach (var (currency, value) in component.BalanceSpent)
+        {
+            component.Balance[currency] += value;
+        }
+        // Reset store back to its original state
+        RefreshAllListings(component);
+        component.BalanceSpent = new();
+        UpdateUserInterface(buyer, uid, component);
+    }
+
+    private void HandleRefundComp(EntityUid uid, StoreComponent component, EntityUid purchase)
+    {
+        component.BoughtEntities.Add(purchase);
+        var refundComp = EnsureComp<StoreRefundComponent>(purchase);
+        refundComp.StoreEntity = uid;
+    }
+
+    private bool IsOnStartingMap(EntityUid store, StoreComponent component)
+    {
+        var xform = Transform(store);
+        return component.StartingMap == xform.MapUid;
+    }
+
+    /// <summary>
+    ///     Disables refunds for this store
+    /// </summary>
+    public void DisableRefund(EntityUid store, StoreComponent? component = null)
+    {
+        if (!Resolve(store, ref component))
+            return;
+
+        component.RefundAllowed = false;
+    }
 }
index 67fbd4faf51074f87b34878127cc641e928dabb3..8ce1f9bb83a09520eddfaed17e7aa81301aac007 100644 (file)
@@ -36,12 +36,14 @@ public sealed partial class StoreSystem : EntitySystem
 
         InitializeUi();
         InitializeCommand();
+        InitializeRefund();
     }
 
     private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent args)
     {
         RefreshAllListings(component);
         InitializeFromPreset(component.Preset, uid, component);
+        component.StartingMap = Transform(uid).MapUid;
     }
 
     private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args)
index b0f72e6dfe680ce2e555841e97f1cde70b2b8157..5dccc253373b96dafe7798721863397eb1fc3405 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Shared.FixedPoint;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -77,6 +78,20 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
     [DataField("productAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
     public string? ProductAction;
 
+    /// <summary>
+    ///     The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
+    ///         upgrade or to use standalone as an upgrade
+    /// </summary>
+    [DataField]
+    public ProtoId<ListingPrototype>? ProductUpgradeID;
+
+    /// <summary>
+    ///     Keeps track of the current action entity this is tied to, for action upgrades
+    /// </summary>
+    [DataField]
+    [NonSerialized]
+    public EntityUid? ProductActionEntity;
+
     /// <summary>
     /// The event that is broadcast when the listing is purchased.
     /// </summary>
@@ -105,6 +120,7 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
             Description != listing.Description ||
             ProductEntity != listing.ProductEntity ||
             ProductAction != listing.ProductAction ||
+            ProductActionEntity != listing.ProductActionEntity ||
             ProductEvent != listing.ProductEvent ||
             RestockTime != listing.RestockTime)
             return false;
@@ -146,6 +162,8 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
             Priority = Priority,
             ProductEntity = ProductEntity,
             ProductAction = ProductAction,
+            ProductUpgradeID = ProductUpgradeID,
+            ProductActionEntity = ProductActionEntity,
             ProductEvent = ProductEvent,
             PurchaseAmount = PurchaseAmount,
             RestockTime = RestockTime,
index a142cf4e4fba887ec4541c594f49d68541453f17..27a8ada1855abf737214f4f2168daa9e5686338c 100644 (file)
@@ -18,11 +18,14 @@ public sealed class StoreUpdateState : BoundUserInterfaceState
 
     public readonly bool ShowFooter;
 
-    public StoreUpdateState(HashSet<ListingData> listings, Dictionary<string, FixedPoint2> balance, bool showFooter)
+    public readonly bool AllowRefund;
+
+    public StoreUpdateState(HashSet<ListingData> listings, Dictionary<string, FixedPoint2> balance, bool showFooter, bool allowRefund)
     {
         Listings = listings;
         Balance = balance;
         ShowFooter = showFooter;
+        AllowRefund = allowRefund;
     }
 }
 
@@ -72,3 +75,12 @@ public sealed class StoreRequestWithdrawMessage : BoundUserInterfaceMessage
         Amount = amount;
     }
 }
+
+/// <summary>
+///     Used when the refund button is pressed
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class StoreRequestRefundMessage : BoundUserInterfaceMessage
+{
+
+}