Adds a refund button & action upgrades for stores (#24518)
* 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>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
13
Content.Server/Store/StoreRefundComponent.cs
Normal file
13
Content.Server/Store/StoreRefundComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
56
Content.Server/Store/Systems/StoreSystem.Refund.cs
Normal file
56
Content.Server/Store/Systems/StoreSystem.Refund.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user