From 2152914acc666d8dd7b1f1a8864d5738fa5ff7c4 Mon Sep 17 00:00:00 2001
From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Date: Wed, 17 Aug 2022 00:34:25 -0400
Subject: [PATCH] Generalized Store System (#10201)
---
.../Revenant/Ui/RevenantBoundUserInterface.cs | 2 -
.../Store/Ui/StoreBoundUserInterface.cs | 79 ++
.../Store/Ui/StoreListingControl.xaml | 21 +
.../Store/Ui/StoreListingControl.xaml.cs | 24 +
Content.Client/Store/Ui/StoreMenu.xaml | 54 ++
Content.Client/Store/Ui/StoreMenu.xaml.cs | 222 +++++
.../Store/Ui/StoreWithdrawWindow.xaml | 16 +
.../Store/Ui/StoreWithdrawWindow.xaml.cs | 102 +++
.../Uplink/UplinkBoundUserInterface.cs | 65 --
.../Traitor/Uplink/UplinkListingControl.xaml | 22 -
.../Uplink/UplinkListingControl.xaml.cs | 28 -
Content.Client/Traitor/Uplink/UplinkMenu.xaml | 53 --
.../Traitor/Uplink/UplinkMenu.xaml.cs | 163 ----
.../Traitor/Uplink/UplinkWithdrawWindow.xaml | 22 -
.../Uplink/UplinkWithdrawWindow.xaml.cs | 34 -
.../GameTicking/Rules/NukeopsRuleSystem.cs | 3 +
.../GameTicking/Rules/SuspicionRuleSystem.cs | 13 +-
.../Rules/TraitorDeathMatchRuleSystem.cs | 34 +-
.../GameTicking/Rules/TraitorRuleSystem.cs | 18 +-
Content.Server/PDA/PDASystem.cs | 36 +-
.../Store/Components/CurrencyComponent.cs | 22 +
.../Store/Components/StoreComponent.cs | 91 +++
.../Store/Conditions/BuyerAntagCondition.cs | 63 ++
.../Store/Conditions/BuyerJobCondition.cs | 63 ++
.../Conditions/BuyerWhitelistCondition.cs | 41 +
.../ListingLimitedStockCondition.cs | 20 +
.../Conditions/StoreWhitelistCondition.cs | 44 +
.../Store/Systems/StoreSystem.Listings.cs | 127 +++
.../Store/Systems/StoreSystem.Ui.cs | 226 ++++++
Content.Server/Store/Systems/StoreSystem.cs | 154 ++++
.../Uplink/Account/UplinkAccountEvents.cs | 30 -
.../Uplink/Account/UplinkAccountsSystem.cs | 108 ---
.../Uplink/Commands/AddUplinkCommand.cs | 14 +-
.../SurplusBundle/SurplusBundleComponent.cs | 10 +
.../SurplusBundle/SurplusBundleSystem.cs | 41 +-
.../Telecrystal/TelecrystalComponent.cs | 7 -
.../Uplink/Telecrystal/TelecrystalSystem.cs | 52 --
.../Traitor/Uplink/UplinkComponent.cs | 38 -
Content.Server/Traitor/Uplink/UplinkEvents.cs | 18 -
.../Traitor/Uplink/UplinkListingSytem.cs | 53 --
Content.Server/Traitor/Uplink/UplinkSystem.cs | 256 +-----
.../TraitorDeathMatchRedemptionSystem.cs | 51 +-
Content.Shared.Database/LogType.cs | 1 +
Content.Shared/PDA/UplinkCategory.cs | 17 -
.../PDA/UplinkStoreListingPrototype.cs | 40 -
Content.Shared/Store/CurrencyPrototype.cs | 43 +
Content.Shared/Store/ListingCondition.cs | 23 +
Content.Shared/Store/ListingPrototype.cs | 133 +++
.../Store/StoreCategoryPrototype.cs | 22 +
Content.Shared/Store/StorePresetPrototype.cs | 41 +
Content.Shared/Store/StoreUi.cs | 84 ++
.../Traitor/Uplink/UplinkAccount.cs | 14 -
.../Traitor/Uplink/UplinkAccountData.cs | 17 -
.../Traitor/Uplink/UplinkListingData.cs | 41 -
.../Traitor/Uplink/UplinkMessagesUI.cs | 35 -
.../Traitor/Uplink/UplinkNetworkEvents.cs | 14 -
.../Traitor/Uplink/UplinkUpdateState.cs | 17 -
.../Traitor/Uplink/UplinkVisuals.cs | 10 -
Resources/Locale/en-US/store/currency.ftl | 11 +
Resources/Locale/en-US/store/store.ftl | 4 +
Resources/Prototypes/Catalog/catalog.yml | 44 +
.../Prototypes/Catalog/uplink_catalog.yml | 761 +++++++++++-------
.../Entities/Objects/Devices/pda.yml | 4 +-
.../Entities/Objects/Specific/syndicate.yml | 54 +-
Resources/Prototypes/Store/categories.yml | 60 ++
Resources/Prototypes/Store/currency.yml | 12 +
Resources/Prototypes/Store/presets.yml | 16 +
Resources/Prototypes/tags.yml | 3 +
68 files changed, 2493 insertions(+), 1568 deletions(-)
create mode 100644 Content.Client/Store/Ui/StoreBoundUserInterface.cs
create mode 100644 Content.Client/Store/Ui/StoreListingControl.xaml
create mode 100644 Content.Client/Store/Ui/StoreListingControl.xaml.cs
create mode 100644 Content.Client/Store/Ui/StoreMenu.xaml
create mode 100644 Content.Client/Store/Ui/StoreMenu.xaml.cs
create mode 100644 Content.Client/Store/Ui/StoreWithdrawWindow.xaml
create mode 100644 Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
delete mode 100644 Content.Client/Traitor/Uplink/UplinkBoundUserInterface.cs
delete mode 100644 Content.Client/Traitor/Uplink/UplinkListingControl.xaml
delete mode 100644 Content.Client/Traitor/Uplink/UplinkListingControl.xaml.cs
delete mode 100644 Content.Client/Traitor/Uplink/UplinkMenu.xaml
delete mode 100644 Content.Client/Traitor/Uplink/UplinkMenu.xaml.cs
delete mode 100644 Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml
delete mode 100644 Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml.cs
create mode 100644 Content.Server/Store/Components/CurrencyComponent.cs
create mode 100644 Content.Server/Store/Components/StoreComponent.cs
create mode 100644 Content.Server/Store/Conditions/BuyerAntagCondition.cs
create mode 100644 Content.Server/Store/Conditions/BuyerJobCondition.cs
create mode 100644 Content.Server/Store/Conditions/BuyerWhitelistCondition.cs
create mode 100644 Content.Server/Store/Conditions/ListingLimitedStockCondition.cs
create mode 100644 Content.Server/Store/Conditions/StoreWhitelistCondition.cs
create mode 100644 Content.Server/Store/Systems/StoreSystem.Listings.cs
create mode 100644 Content.Server/Store/Systems/StoreSystem.Ui.cs
create mode 100644 Content.Server/Store/Systems/StoreSystem.cs
delete mode 100644 Content.Server/Traitor/Uplink/Account/UplinkAccountEvents.cs
delete mode 100644 Content.Server/Traitor/Uplink/Account/UplinkAccountsSystem.cs
delete mode 100644 Content.Server/Traitor/Uplink/Telecrystal/TelecrystalComponent.cs
delete mode 100644 Content.Server/Traitor/Uplink/Telecrystal/TelecrystalSystem.cs
delete mode 100644 Content.Server/Traitor/Uplink/UplinkComponent.cs
delete mode 100644 Content.Server/Traitor/Uplink/UplinkEvents.cs
delete mode 100644 Content.Server/Traitor/Uplink/UplinkListingSytem.cs
delete mode 100644 Content.Shared/PDA/UplinkCategory.cs
delete mode 100644 Content.Shared/PDA/UplinkStoreListingPrototype.cs
create mode 100644 Content.Shared/Store/CurrencyPrototype.cs
create mode 100644 Content.Shared/Store/ListingCondition.cs
create mode 100644 Content.Shared/Store/ListingPrototype.cs
create mode 100644 Content.Shared/Store/StoreCategoryPrototype.cs
create mode 100644 Content.Shared/Store/StorePresetPrototype.cs
create mode 100644 Content.Shared/Store/StoreUi.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkAccount.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkAccountData.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkListingData.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkMessagesUI.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkNetworkEvents.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkUpdateState.cs
delete mode 100644 Content.Shared/Traitor/Uplink/UplinkVisuals.cs
create mode 100644 Resources/Locale/en-US/store/currency.ftl
create mode 100644 Resources/Locale/en-US/store/store.ftl
create mode 100644 Resources/Prototypes/Catalog/catalog.yml
create mode 100644 Resources/Prototypes/Store/categories.yml
create mode 100644 Resources/Prototypes/Store/currency.yml
create mode 100644 Resources/Prototypes/Store/presets.yml
diff --git a/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs b/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs
index 9fc45e3aff..17f7d88875 100644
--- a/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs
+++ b/Content.Client/Revenant/Ui/RevenantBoundUserInterface.cs
@@ -1,6 +1,4 @@
-using Content.Client.Traitor.Uplink;
using Content.Shared.Revenant;
-using Content.Shared.Traitor.Uplink;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
new file mode 100644
index 0000000000..0122decdc2
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
@@ -0,0 +1,79 @@
+using Content.Shared.Store;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using System.Linq;
+
+namespace Content.Client.Store.Ui;
+
+[UsedImplicitly]
+public sealed class StoreBoundUserInterface : BoundUserInterface
+{
+ private StoreMenu? _menu;
+
+ private string _windowName = Loc.GetString("store-ui-default-title");
+
+ public StoreBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
+ {
+
+ }
+
+ protected override void Open()
+ {
+ _menu = new StoreMenu(_windowName);
+
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+
+ _menu.OnListingButtonPressed += (_, listing) =>
+ {
+ if (_menu.CurrentBuyer != null)
+ SendMessage(new StoreBuyListingMessage(_menu.CurrentBuyer.Value, listing));
+ };
+
+ _menu.OnCategoryButtonPressed += (_, category) =>
+ {
+ _menu.CurrentCategory = category;
+ if (_menu.CurrentBuyer != null)
+ SendMessage(new StoreRequestUpdateInterfaceMessage(_menu.CurrentBuyer.Value));
+ };
+
+ _menu.OnWithdrawAttempt += (_, type, amount) =>
+ {
+ if (_menu.CurrentBuyer != null)
+ SendMessage(new StoreRequestWithdrawMessage(_menu.CurrentBuyer.Value, type, amount));
+ };
+ }
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (_menu == null)
+ return;
+
+ switch (state)
+ {
+ case StoreUpdateState msg:
+ if (msg.Buyer != null)
+ _menu.CurrentBuyer = msg.Buyer;
+ _menu.UpdateBalance(msg.Balance);
+ _menu.PopulateStoreCategoryButtons(msg.Listings);
+ _menu.UpdateListing(msg.Listings.ToList());
+ break;
+ case StoreInitializeState msg:
+ _windowName = msg.Name;
+ if (_menu != null && _menu.Window != null)
+ _menu.Window.Title = msg.Name;
+ break;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Close();
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml b/Content.Client/Store/Ui/StoreListingControl.xaml
new file mode 100644
index 0000000000..aefeec17cc
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml.cs b/Content.Client/Store/Ui/StoreListingControl.xaml.cs
new file mode 100644
index 0000000000..073d627439
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml.cs
@@ -0,0 +1,24 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Store.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class StoreListingControl : Control
+{
+ public StoreListingControl(string itemName, string itemDescription,
+ string price, bool canBuy, Texture? texture = null)
+ {
+ RobustXamlLoader.Load(this);
+
+ StoreItemName.Text = itemName;
+ StoreItemDescription.SetMessage(itemDescription);
+
+ StoreItemBuyButton.Text = price;
+ StoreItemBuyButton.Disabled = !canBuy;
+
+ StoreItemTexture.Texture = texture;
+ }
+}
diff --git a/Content.Client/Store/Ui/StoreMenu.xaml b/Content.Client/Store/Ui/StoreMenu.xaml
new file mode 100644
index 0000000000..fbbf6c1343
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreMenu.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs
new file mode 100644
index 0000000000..b916b8e890
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs
@@ -0,0 +1,222 @@
+using Content.Client.Message;
+using Content.Shared.Store;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Client.Graphics;
+using Content.Shared.Actions.ActionTypes;
+using System.Linq;
+using Content.Shared.FixedPoint;
+
+namespace Content.Client.Store.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class StoreMenu : DefaultWindow
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private StoreWithdrawWindow? _withdrawWindow;
+
+ public event Action? OnListingButtonPressed;
+ public event Action? OnCategoryButtonPressed;
+ public event Action? OnWithdrawAttempt;
+
+ public EntityUid? CurrentBuyer;
+ public Dictionary Balance = new();
+ public string CurrentCategory = string.Empty;
+
+ public StoreMenu(string name)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
+ if (Window != null)
+ Window.Title = name;
+ }
+
+ public void UpdateBalance(Dictionary balance)
+ {
+ Balance = balance;
+
+ var currency = new Dictionary<(string, FixedPoint2), CurrencyPrototype>();
+ foreach (var type in balance)
+ {
+ currency.Add((type.Key, type.Value), _prototypeManager.Index(type.Key));
+ }
+
+ var balanceStr = string.Empty;
+ foreach (var type in currency)
+ {
+ balanceStr += $"{Loc.GetString(type.Value.BalanceDisplay, ("amount", type.Key.Item2))}\n";
+ }
+
+ BalanceInfo.SetMarkup(balanceStr.TrimEnd());
+
+ var disabled = true;
+ foreach (var type in currency)
+ {
+ if (type.Value.CanWithdraw && type.Value.EntityId != null && type.Key.Item2 > 0)
+ disabled = false;
+ }
+
+ WithdrawButton.Disabled = disabled;
+ }
+
+ public void UpdateListing(List listings)
+ {
+ var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
+
+ // should probably chunk these out instead. to-do if this clogs the internet tubes.
+ // maybe read clients prototypes instead?
+ ClearListings();
+ foreach (var item in sorted)
+ {
+ AddListingGui(item);
+ }
+ }
+
+ private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
+ {
+ // check if window is already open
+ if (_withdrawWindow != null && _withdrawWindow.IsOpen)
+ {
+ _withdrawWindow.MoveToFront();
+ return;
+ }
+
+ // open a new one
+ _withdrawWindow = new StoreWithdrawWindow();
+ _withdrawWindow.OpenCentered();
+
+ _withdrawWindow.CreateCurrencyButtons(Balance);
+ _withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
+ }
+
+ private void AddListingGui(ListingData listing)
+ {
+ if (!listing.Categories.Contains(CurrentCategory))
+ return;
+
+ string listingName = new (listing.Name);
+ string listingDesc = new (listing.Description);
+ var listingPrice = listing.Cost;
+ var canBuy = CanBuyListing(Balance, listingPrice);
+
+ var spriteSys = _entityManager.EntitySysManager.GetEntitySystem();
+
+ Texture? texture = null;
+ if (listing.Icon != null)
+ texture = spriteSys.Frame0(listing.Icon);
+
+ if (listing.ProductEntity != null)
+ {
+ if (texture == null)
+ texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
+
+ var proto = _prototypeManager.Index(listing.ProductEntity);
+ if (listingName == string.Empty)
+ listingName = proto.Name;
+ if (listingDesc == string.Empty)
+ listingDesc = proto.Description;
+ }
+ else if (listing.ProductAction != null)
+ {
+ var action = _prototypeManager.Index(listing.ProductAction);
+ if (action.Icon != null)
+ texture = spriteSys.Frame0(action.Icon);
+ }
+
+ var newListing = new StoreListingControl(listingName, listingDesc, GetListingPriceString(listing), canBuy, texture);
+ newListing.StoreItemBuyButton.OnButtonDown += args
+ => OnListingButtonPressed?.Invoke(args, listing);
+
+ StoreListingsContainer.AddChild(newListing);
+ }
+
+ public bool CanBuyListing(Dictionary currency, Dictionary price)
+ {
+ foreach (var type in price)
+ {
+ if (!currency.ContainsKey(type.Key))
+ return false;
+
+ if (currency[type.Key] < type.Value)
+ return false;
+ }
+ return true;
+ }
+
+ public string GetListingPriceString(ListingData listing)
+ {
+ var text = string.Empty;
+
+ foreach (var type in listing.Cost)
+ {
+ var currency = _prototypeManager.Index(type.Key);
+ text += $"{Loc.GetString(currency.PriceDisplay, ("amount", type.Value))}\n";
+ }
+
+ if (listing.Cost.Count < 1)
+ text = Loc.GetString("store-currency-free");
+
+ return text.TrimEnd();
+ }
+
+ private void ClearListings()
+ {
+ StoreListingsContainer.Children.Clear();
+ }
+
+ public void PopulateStoreCategoryButtons(HashSet listings)
+ {
+ var allCategories = new List();
+ foreach (var listing in listings)
+ {
+ foreach (var cat in listing.Categories)
+ {
+ var proto = _prototypeManager.Index(cat);
+ if (!allCategories.Contains(proto))
+ allCategories.Add(proto);
+ }
+ }
+
+ allCategories = allCategories.OrderBy(c => c.Priority).ToList();
+
+ if (CurrentCategory == string.Empty && allCategories.Count > 0)
+ CurrentCategory = allCategories.First().ID;
+
+ if (allCategories.Count <= 1)
+ return;
+
+ CategoryListContainer.Children.Clear();
+
+ foreach (var proto in allCategories)
+ {
+ var catButton = new StoreCategoryButton
+ {
+ Text = Loc.GetString(proto.Name),
+ Id = proto.ID
+ };
+
+ catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
+ CategoryListContainer.AddChild(catButton);
+ }
+ }
+
+ public override void Close()
+ {
+ base.Close();
+ CurrentBuyer = null;
+ _withdrawWindow?.Close();
+ }
+
+ private sealed class StoreCategoryButton : Button
+ {
+ public string? Id;
+ }
+}
diff --git a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml
new file mode 100644
index 0000000000..ac68c1e237
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
new file mode 100644
index 0000000000..968e3ed610
--- /dev/null
+++ b/Content.Client/Store/Ui/StoreWithdrawWindow.xaml.cs
@@ -0,0 +1,102 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Localization;
+using Content.Shared.FixedPoint;
+using Content.Shared.Store;
+using Robust.Client.UserInterface;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Client.Graphics;
+using Content.Shared.Actions.ActionTypes;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Store.Ui;
+
+///
+/// Window to select amount TC to withdraw from Uplink account
+/// Used as sub-window in Uplink UI
+///
+[GenerateTypedNameReferences]
+public sealed partial class StoreWithdrawWindow : DefaultWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private Dictionary _validCurrencies = new();
+ private HashSet _buttons = new();
+ public event Action? OnWithdrawAttempt;
+
+ public StoreWithdrawWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void CreateCurrencyButtons(Dictionary balance)
+ {
+ _validCurrencies.Clear();
+ foreach (var currency in balance)
+ {
+ if (!_prototypeManager.TryIndex(currency.Key, out var proto))
+ continue;
+
+ _validCurrencies.Add(currency.Value, proto);
+ }
+
+ //this shouldn't ever happen but w/e
+ if (_validCurrencies.Count < 1)
+ return;
+
+ ButtonContainer.Children.Clear();
+ _buttons.Clear();
+ foreach (var currency in _validCurrencies)
+ {
+ Logger.Debug((currency.Value.PriceDisplay));
+ var button = new CurrencyWithdrawButton()
+ {
+ Id = currency.Value.ID,
+ Amount = currency.Key,
+ MinHeight = 20,
+ Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.PriceDisplay))),
+ };
+ button.Disabled = false;
+ button.OnPressed += args =>
+ {
+ OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
+ Close();
+ };
+
+ _buttons.Add(button);
+ ButtonContainer.AddChild(button);
+ }
+
+ var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
+
+ // setup withdraw slider
+ WithdrawSlider.MinValue = 1;
+ WithdrawSlider.MaxValue = maxWithdrawAmount;
+
+ WithdrawSlider.OnValueChanged += OnValueChanged;
+ OnValueChanged(WithdrawSlider.Value);
+ }
+
+ public void OnValueChanged(int i)
+ {
+ foreach (var button in _buttons)
+ {
+ button.Disabled = button.Amount < WithdrawSlider.Value;
+ }
+ }
+
+ private sealed class CurrencyWithdrawButton : Button
+ {
+ public string? Id;
+ public FixedPoint2 Amount = FixedPoint2.Zero;
+ }
+}
diff --git a/Content.Client/Traitor/Uplink/UplinkBoundUserInterface.cs b/Content.Client/Traitor/Uplink/UplinkBoundUserInterface.cs
deleted file mode 100644
index c7051756f7..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkBoundUserInterface.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Content.Shared.Traitor.Uplink;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.Traitor.Uplink
-{
- [UsedImplicitly]
- public sealed class UplinkBoundUserInterface : BoundUserInterface
- {
- private UplinkMenu? _menu;
-
- public UplinkBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
- {
-
- }
-
- protected override void Open()
- {
- _menu = new UplinkMenu();
- _menu.OpenCentered();
- _menu.OnClose += Close;
-
- _menu.OnListingButtonPressed += (_, listing) =>
- {
- SendMessage(new UplinkBuyListingMessage(listing.ItemId));
- };
-
- _menu.OnCategoryButtonPressed += (_, category) =>
- {
- _menu.CurrentFilterCategory = category;
- SendMessage(new UplinkRequestUpdateInterfaceMessage());
- };
-
- _menu.OnWithdrawAttempt += (tc) =>
- {
- SendMessage(new UplinkTryWithdrawTC(tc));
- };
- }
- protected override void UpdateState(BoundUserInterfaceState state)
- {
- base.UpdateState(state);
-
- if (_menu == null)
- return;
-
- switch (state)
- {
- case UplinkUpdateState msg:
- _menu.UpdateAccount(msg.Account);
- _menu.UpdateListing(msg.Listings);
- break;
- }
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (!disposing)
- return;
-
- _menu?.Close();
- _menu?.Dispose();
- }
- }
-}
diff --git a/Content.Client/Traitor/Uplink/UplinkListingControl.xaml b/Content.Client/Traitor/Uplink/UplinkListingControl.xaml
deleted file mode 100644
index d6d1283ced..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkListingControl.xaml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Traitor/Uplink/UplinkListingControl.xaml.cs b/Content.Client/Traitor/Uplink/UplinkListingControl.xaml.cs
deleted file mode 100644
index 0bb269e58a..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkListingControl.xaml.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Robust.Client.AutoGenerated;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Maths;
-
-namespace Content.Client.Traitor.Uplink
-{
- [GenerateTypedNameReferences]
- public sealed partial class UplinkListingControl : Control
- {
-
- public UplinkListingControl(string itemName, string itemDescription,
- int itemPrice, bool canBuy, Texture? texture = null)
- {
- RobustXamlLoader.Load(this);
-
- UplinkItemName.Text = itemName;
- UplinkItemDescription.SetMessage(itemDescription);
-
- UplinkItemBuyButton.Text = $"{itemPrice} TC";
- UplinkItemBuyButton.Disabled = !canBuy;
-
- UplinkItemTexture.Texture = texture;
- }
- }
-}
diff --git a/Content.Client/Traitor/Uplink/UplinkMenu.xaml b/Content.Client/Traitor/Uplink/UplinkMenu.xaml
deleted file mode 100644
index 9ea5e2869e..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkMenu.xaml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Traitor/Uplink/UplinkMenu.xaml.cs b/Content.Client/Traitor/Uplink/UplinkMenu.xaml.cs
deleted file mode 100644
index 14c8c072df..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkMenu.xaml.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using Content.Client.Message;
-using Content.Shared.PDA;
-using Content.Shared.Traitor.Uplink;
-using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Client.Utility;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Traitor.Uplink
-{
- [GenerateTypedNameReferences]
- public sealed partial class UplinkMenu : DefaultWindow
- {
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IResourceCache _resourceCache = default!;
-
- private UplinkWithdrawWindow? _withdrawWindow;
-
- public event Action? OnListingButtonPressed;
- public event Action? OnCategoryButtonPressed;
- public event Action? OnWithdrawAttempt;
-
- private UplinkCategory _currentFilter;
- private UplinkAccountData? _loggedInUplinkAccount;
-
- public UplinkMenu()
- {
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
-
- PopulateUplinkCategoryButtons();
- WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
- }
-
- public UplinkCategory CurrentFilterCategory
- {
- get => _currentFilter;
- set
- {
- if (value.GetType() != typeof(UplinkCategory))
- {
- return;
- }
-
- _currentFilter = value;
- }
- }
-
- public void UpdateAccount(UplinkAccountData account)
- {
- _loggedInUplinkAccount = account;
-
- // update balance label
- var balance = account.DataBalance;
- var weightedColor = balance switch
- {
- <= 0 => "gray",
- <= 5 => "green",
- <= 20 => "yellow",
- <= 50 => "purple",
- _ => "gray"
- };
- var balanceStr = Loc.GetString("uplink-bound-user-interface-tc-balance-popup",
- ("weightedColor", weightedColor),
- ("balance", balance));
- BalanceInfo.SetMarkup(balanceStr);
-
- // you can't withdraw if you don't have TC
- WithdrawButton.Disabled = balance <= 0;
- }
-
- public void UpdateListing(UplinkListingData[] listings)
- {
- // should probably chunk these out instead. to-do if this clogs the internet tubes.
- // maybe read clients prototypes instead?
- ClearListings();
- foreach (var item in listings)
- {
- AddListingGui(item);
- }
- }
-
- private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
- {
- if (_loggedInUplinkAccount == null)
- return;
-
- // check if window is already open
- if (_withdrawWindow != null && _withdrawWindow.IsOpen)
- {
- _withdrawWindow.MoveToFront();
- return;
- }
-
- // open a new one
- _withdrawWindow = new UplinkWithdrawWindow(_loggedInUplinkAccount.DataBalance);
- _withdrawWindow.OpenCentered();
-
- _withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
- }
-
- private void AddListingGui(UplinkListingData listing)
- {
- if (!_prototypeManager.TryIndex(listing.ItemId, out EntityPrototype? prototype) || listing.Category != CurrentFilterCategory)
- {
- return;
- }
-
- var listingName = listing.ListingName == string.Empty ? prototype.Name : listing.ListingName;
- var listingDesc = listing.Description == string.Empty ? prototype.Description : listing.Description;
- var listingPrice = listing.Price;
- var canBuy = _loggedInUplinkAccount?.DataBalance >= listing.Price;
-
- var texture = listing.Icon?.Frame0();
- if (texture == null)
- texture = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
-
- var newListing = new UplinkListingControl(listingName, listingDesc, listingPrice, canBuy, texture);
- newListing.UplinkItemBuyButton.OnButtonDown += args
- => OnListingButtonPressed?.Invoke(args, listing);
-
- UplinkListingsContainer.AddChild(newListing);
- }
-
- private void ClearListings()
- {
- UplinkListingsContainer.Children.Clear();
- }
-
- private void PopulateUplinkCategoryButtons()
- {
- foreach (UplinkCategory cat in Enum.GetValues(typeof(UplinkCategory)))
- {
- var catButton = new PDAUplinkCategoryButton
- {
- Text = Loc.GetString(cat.ToString()),
- ButtonCategory = cat
- };
- //It'd be neat if it could play a cool tech ping sound when you switch categories,
- //but right now there doesn't seem to be an easy way to do client-side audio without still having to round trip to the server and
- //send to a specific client INetChannel.
- catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.ButtonCategory);
-
- CategoryListContainer.AddChild(catButton);
- }
- }
-
- public override void Close()
- {
- base.Close();
- _withdrawWindow?.Close();
- }
-
- private sealed class PDAUplinkCategoryButton : Button
- {
- public UplinkCategory ButtonCategory;
- }
- }
-}
diff --git a/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml b/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml
deleted file mode 100644
index 92d94e112d..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml.cs b/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml.cs
deleted file mode 100644
index d83546dd55..0000000000
--- a/Content.Client/Traitor/Uplink/UplinkWithdrawWindow.xaml.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
-
-namespace Content.Client.Traitor.Uplink
-{
- ///
- /// Window to select amount TC to withdraw from Uplink account
- /// Used as sub-window in Uplink UI
- ///
- [GenerateTypedNameReferences]
- public sealed partial class UplinkWithdrawWindow : DefaultWindow
- {
- public event System.Action? OnWithdrawAttempt;
-
- public UplinkWithdrawWindow(int tcCount)
- {
- RobustXamlLoader.Load(this);
-
- // setup withdraw slider
- WithdrawSlider.MinValue = 1;
- WithdrawSlider.MaxValue = tcCount;
-
- // and buttons
- ApplyButton.OnButtonDown += _ =>
- {
- OnWithdrawAttempt?.Invoke(WithdrawSlider.Value);
- Close();
- };
- CancelButton.OnButtonDown += _ => Close();
- }
- }
-}
diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
index 71c7d46664..3ca27a4a46 100644
--- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
@@ -20,6 +20,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Content.Server.Traitor;
+using System.Data;
+using Content.Server.Traitor.Uplink;
using Robust.Shared.Audio;
using Robust.Shared.Player;
@@ -35,6 +37,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
+ [Dependency] private readonly UplinkSystem _uplink = default!;
private Dictionary _aliveNukeops = new();
private bool _opsWon;
diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
index c60da5e71c..d77d5f9d2b 100644
--- a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
@@ -8,7 +8,6 @@ using Content.Server.Station.Components;
using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink;
-using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar;
using Content.Shared.Doors.Systems;
using Content.Shared.EntityList;
@@ -17,7 +16,6 @@ using Content.Shared.Maps;
using Content.Shared.MobState.Components;
using Content.Shared.Roles;
using Content.Shared.Suspicion;
-using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
@@ -48,6 +46,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
+ [Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "Suspicion";
@@ -173,16 +172,8 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
mind!.AddRole(traitorRole);
traitors.Add(traitorRole);
- // creadth: we need to create uplink for the antag.
- // PDA should be in place already, so we just need to
- // initiate uplink account.
- var uplinkAccount = new UplinkAccount(traitorStartingBalance, mind.OwnedEntity!);
- var accounts = EntityManager.EntitySysManager.GetEntitySystem();
- accounts.AddNewAccount(uplinkAccount);
-
// try to place uplink
- if (!EntityManager.EntitySysManager.GetEntitySystem()
- .AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
+ if (!_uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance))
continue;
}
diff --git a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
index 92b6913fc1..1cd87250c4 100644
--- a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Configurations;
@@ -6,19 +6,18 @@ using Content.Server.Hands.Components;
using Content.Server.PDA;
using Content.Server.Players;
using Content.Server.Spawners.Components;
+using Content.Server.Store.Components;
using Content.Server.Traitor;
using Content.Server.Traitor.Uplink;
-using Content.Server.Traitor.Uplink.Account;
-using Content.Server.Traitor.Uplink.Components;
using Content.Server.TraitorDeathMatch.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.MobState.Components;
using Content.Shared.PDA;
using Content.Shared.Roles;
-using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
@@ -39,6 +38,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
+ [Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "TraitorDeathMatch";
@@ -48,7 +48,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
private bool _safeToEndRound = false;
- private readonly Dictionary _allOriginalNames = new();
+ private readonly Dictionary _allOriginalNames = new();
private const string TraitorPrototypeID = "Traitor";
@@ -108,15 +108,10 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
newTmp = Spawn(BackpackPrototypeName, ownedCoords);
_inventory.TryEquip(owned, newTmp, "back", true);
- // Like normal traitors, they need access to a traitor account.
- var uplinkAccount = new UplinkAccount(startingBalance, owned);
- var accounts = EntityManager.EntitySysManager.GetEntitySystem();
- accounts.AddNewAccount(uplinkAccount);
+ if (!_uplink.AddUplink(owned, startingBalance))
+ return;
- EntityManager.EntitySysManager.GetEntitySystem()
- .AddUplink(owned, uplinkAccount, newPDA);
-
- _allOriginalNames[uplinkAccount] = Name(owned);
+ _allOriginalNames[owned] = Name(owned);
// The PDA needs to be marked with the correct owner.
var pda = Comp(newPDA);
@@ -186,14 +181,17 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
var lines = new List();
lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
- foreach (var uplink in EntityManager.EntityQuery(true))
+
+ foreach (var uplink in EntityManager.EntityQuery(true))
{
- var uplinkAcc = uplink.UplinkAccount;
- if (uplinkAcc != null && _allOriginalNames.ContainsKey(uplinkAcc))
+ var owner = uplink.AccountOwner;
+ if (owner != null && _allOriginalNames.ContainsKey(owner.Value))
{
+ var tcbalance = _uplink.GetTCBalance(uplink);
+
lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
- ("originalName", _allOriginalNames[uplinkAcc]),
- ("tcBalance", uplinkAcc.Balance)));
+ ("originalName", _allOriginalNames[owner.Value]),
+ ("tcBalance", tcbalance)));
}
}
diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
index 030e1c2300..29b7b91895 100644
--- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
@@ -3,13 +3,14 @@ using Content.Server.Chat.Managers;
using Content.Server.Objectives.Interfaces;
using Content.Server.Players;
using Content.Server.Roles;
+using Content.Server.Store.Systems;
using Content.Server.Traitor;
using Content.Server.Traitor.Uplink;
-using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar;
using Content.Shared.Dataset;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Inventory;
using Content.Shared.Roles;
-using Content.Shared.Traitor.Uplink;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
@@ -28,6 +29,10 @@ public sealed class TraitorRuleSystem : GameRuleSystem
[Dependency] private readonly IObjectivesManager _objectivesManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly StoreSystem _store = default!;
+ [Dependency] private readonly UplinkSystem _uplink = default!;
public override string Prototype => "Traitor";
@@ -35,6 +40,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
public List Traitors = new();
private const string TraitorPrototypeID = "Traitor";
+ private const string TraitorUplinkPresetId = "StorePresetUplink";
public int TotalTraitors => Traitors.Count;
public string[] Codewords = new string[3];
@@ -173,16 +179,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem
}
// creadth: we need to create uplink for the antag.
- // PDA should be in place already, so we just need to
- // initiate uplink account.
+ // PDA should be in place already
DebugTools.AssertNotNull(mind.OwnedEntity);
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
- var uplinkAccount = new UplinkAccount(startingBalance, mind.OwnedEntity!);
- var accounts = EntityManager.EntitySysManager.GetEntitySystem();
- accounts.AddNewAccount(uplinkAccount);
- if (!EntityManager.EntitySysManager.GetEntitySystem().AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
+ if (!_uplink.AddUplink(mind.OwnedEntity!.Value, startingBalance))
return false;
var antagPrototype = _prototypeManager.Index(TraitorPrototypeID);
diff --git a/Content.Server/PDA/PDASystem.cs b/Content.Server/PDA/PDASystem.cs
index 3421380f34..84d0e95392 100644
--- a/Content.Server/PDA/PDASystem.cs
+++ b/Content.Server/PDA/PDASystem.cs
@@ -2,29 +2,28 @@ using Content.Server.Instruments;
using Content.Server.Light.Components;
using Content.Server.Light.EntitySystems;
using Content.Server.Light.Events;
-using Content.Server.Traitor.Uplink;
-using Content.Server.Traitor.Uplink.Account;
-using Content.Server.Traitor.Uplink.Components;
using Content.Server.PDA.Ringer;
-using Content.Server.Station.Components;
+using Content.Server.Store.Components;
+using Content.Server.Store.Systems;
using Content.Server.Station.Systems;
using Content.Server.UserInterface;
using Content.Shared.PDA;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
+using Content.Server.Mind.Components;
+using Content.Server.Traitor;
namespace Content.Server.PDA
{
public sealed class PDASystem : SharedPDASystem
{
- [Dependency] private readonly UplinkSystem _uplinkSystem = default!;
- [Dependency] private readonly UplinkAccountsSystem _uplinkAccounts = default!;
[Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
[Dependency] private readonly RingerSystem _ringerSystem = default!;
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
+ [Dependency] private readonly StoreSystem _storeSystem = default!;
public override void Initialize()
{
@@ -32,8 +31,8 @@ namespace Content.Server.PDA
SubscribeLocalEvent(OnLightToggle);
SubscribeLocalEvent(AfterUIOpen);
- SubscribeLocalEvent(OnUplinkInit);
- SubscribeLocalEvent(OnUplinkRemoved);
+ SubscribeLocalEvent(OnUplinkInit);
+ SubscribeLocalEvent(OnUplinkRemoved);
SubscribeLocalEvent(OnGridChanged);
}
@@ -74,12 +73,12 @@ namespace Content.Server.PDA
UpdatePDAUserInterface(pda);
}
- private void OnUplinkInit(EntityUid uid, PDAComponent pda, UplinkInitEvent args)
+ private void OnUplinkInit(EntityUid uid, PDAComponent pda, StoreAddedEvent args)
{
UpdatePDAUserInterface(pda);
}
- private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, UplinkRemovedEvent args)
+ private void OnUplinkRemoved(EntityUid uid, PDAComponent pda, StoreRemovedEvent args)
{
UpdatePDAUserInterface(pda);
}
@@ -111,7 +110,7 @@ namespace Content.Server.PDA
// players. This should really use a sort of key-code entry system that selects an account which is not directly tied to
// a player entity.
- if (!HasComp(pda.Owner))
+ if (!TryComp(pda.Owner, out var storeComponent))
return;
var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument);
@@ -121,7 +120,8 @@ namespace Content.Server.PDA
if (session.AttachedEntity is not EntityUid { Valid: true } user)
continue;
- if (_uplinkAccounts.HasAccount(user))
+ if (storeComponent.AccountOwner == user || (TryComp(session.AttachedEntity, out var mindcomp) && mindcomp.Mind != null &&
+ mindcomp.Mind.HasRole()))
ui.SetState(uplinkState, session);
}
}
@@ -143,8 +143,9 @@ namespace Content.Server.PDA
case PDAShowUplinkMessage _:
{
- if (EntityManager.TryGetComponent(pda.Owner, out UplinkComponent? uplink))
- _uplinkSystem.ToggleUplinkUI(uplink, msg.Session);
+ if (msg.Session.AttachedEntity != null &&
+ TryComp(pda.Owner, out var store))
+ _storeSystem.ToggleUi(msg.Session.AttachedEntity.Value, store);
break;
}
case PDAShowRingtoneMessage _:
@@ -170,8 +171,13 @@ namespace Content.Server.PDA
private void AfterUIOpen(EntityUid uid, PDAComponent pda, AfterActivatableUIOpenEvent args)
{
+ //TODO: this is awful
// A new user opened the UI --> Check if they are a traitor and should get a user specific UI state override.
- if (!HasComp(pda.Owner) || !_uplinkAccounts.HasAccount(args.User))
+ if (!TryComp(pda.Owner, out var storeComp))
+ return;
+
+ if (storeComp.AccountOwner != args.User &&
+ !(TryComp(args.User, out var mindcomp) && mindcomp.Mind != null && mindcomp.Mind.HasRole()))
return;
if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui))
diff --git a/Content.Server/Store/Components/CurrencyComponent.cs b/Content.Server/Store/Components/CurrencyComponent.cs
new file mode 100644
index 0000000000..872a995bde
--- /dev/null
+++ b/Content.Server/Store/Components/CurrencyComponent.cs
@@ -0,0 +1,22 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.Store;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+
+namespace Content.Server.Store.Components;
+
+///
+/// Identifies a component that can be inserted into a store
+/// to increase its balance.
+///
+[RegisterComponent]
+public sealed class CurrencyComponent : Component
+{
+ ///
+ /// The value of the currency.
+ /// The string is the currency type that will be added.
+ /// The FixedPoint2 is the value of each individual currency entity.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("price", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary Price = new();
+}
diff --git a/Content.Server/Store/Components/StoreComponent.cs b/Content.Server/Store/Components/StoreComponent.cs
new file mode 100644
index 0000000000..10b46e3e05
--- /dev/null
+++ b/Content.Server/Store/Components/StoreComponent.cs
@@ -0,0 +1,91 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.Store;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Audio;
+
+namespace Content.Server.Store.Components;
+
+///
+/// This component manages a store which players can use to purchase different listings
+/// through the ui. The currency, listings, and categories are defined in yaml.
+///
+[RegisterComponent]
+public sealed class StoreComponent : Component
+{
+ ///
+ /// The default preset for the store. Is overriden by default values specified on the component.
+ ///
+ [DataField("preset", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? Preset;
+
+ ///
+ /// All the listing categories that are available on this store.
+ /// The available listings are partially based on the categories.
+ ///
+ [DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet Categories = new();
+
+ ///
+ /// The total amount of currency that can be used in the store.
+ /// The string represents the ID of te currency prototype, where the
+ /// float is that amount.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("balance", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary Balance = new();
+
+ ///
+ /// The list of currencies that can be inserted into this store.
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet CurrencyWhitelist = new();
+
+ ///
+ /// The person who "owns" the store/account. Used if you want the listings to be fixed
+ /// regardless of who activated it. I.E. role specific items for uplinks.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public EntityUid? AccountOwner = null;
+
+ ///
+ /// All listings, including those that aren't available to the buyer
+ ///
+ public HashSet Listings = new();
+
+ ///
+ /// All available listings from the last time that it was checked.
+ ///
+ [ViewVariables]
+ public HashSet LastAvailableListings = new();
+
+ ///
+ /// checks whether or not the store has been opened yet.
+ ///
+ public bool Opened = false;
+
+ #region audio
+ ///
+ /// The sound played to the buyer when a purchase is succesfully made.
+ ///
+ [ViewVariables]
+ [DataField("buySuccessSound")]
+ public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg");
+
+ ///
+ /// The sound played to the buyer when a purchase fails.
+ ///
+ [ViewVariables]
+ [DataField("insufficientFundsSound")]
+ public SoundSpecifier InsufficientFundsSound = new SoundPathSpecifier("/Audio/Effects/error.ogg");
+ #endregion
+}
+
+///
+/// Event that is broadcast when a store is added to an entity
+///
+public sealed class StoreAddedEvent : EntityEventArgs { };
+///
+/// Event that is broadcast when a store is removed from an entity
+///
+public sealed class StoreRemovedEvent : EntityEventArgs { };
diff --git a/Content.Server/Store/Conditions/BuyerAntagCondition.cs b/Content.Server/Store/Conditions/BuyerAntagCondition.cs
new file mode 100644
index 0000000000..d0f4292dbe
--- /dev/null
+++ b/Content.Server/Store/Conditions/BuyerAntagCondition.cs
@@ -0,0 +1,63 @@
+using Content.Server.Mind.Components;
+using Content.Server.Traitor;
+using Content.Shared.Roles;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+
+namespace Content.Shared.Store.Conditions;
+
+///
+/// Allows a store entry to be filtered out based on the user's antag role.
+/// Supports both blacklists and whitelists. This is copypaste because roles
+/// are absolute shitcode. Refactor this later. -emo
+///
+public sealed class BuyerAntagCondition : ListingCondition
+{
+ ///
+ /// A whitelist of antag roles that can purchase this listing. Only one needs to be found.
+ ///
+ [DataField("whitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet? Whitelist;
+
+ ///
+ /// A blacklist of antag roles that cannot purchase this listing. Only one needs to be found.
+ ///
+ [DataField("blacklist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet? Blacklist;
+
+ public override bool Condition(ListingConditionArgs args)
+ {
+ var ent = args.EntityManager;
+
+ if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null)
+ return true;
+
+ if (Blacklist != null)
+ {
+ foreach (var role in mind.Mind.AllRoles)
+ {
+ if (role is not TraitorRole blacklistantag)
+ continue;
+
+ if (Blacklist.Contains(blacklistantag.Prototype.ID))
+ return false;
+ }
+ }
+
+ if (Whitelist != null)
+ {
+ var found = false;
+ foreach (var role in mind.Mind.AllRoles)
+ {
+ if (role is not TraitorRole antag)
+ continue;
+
+ if (Whitelist.Contains(antag.Prototype.ID))
+ found = true;
+ }
+ if (!found)
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Server/Store/Conditions/BuyerJobCondition.cs b/Content.Server/Store/Conditions/BuyerJobCondition.cs
new file mode 100644
index 0000000000..d867a65d33
--- /dev/null
+++ b/Content.Server/Store/Conditions/BuyerJobCondition.cs
@@ -0,0 +1,63 @@
+using Content.Server.Mind.Components;
+using Content.Server.Roles;
+using Content.Shared.Roles;
+using Content.Shared.Store;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+
+namespace Content.Server.Store.Conditions;
+
+///
+/// Allows a store entry to be filtered out based on the user's job.
+/// Supports both blacklists and whitelists
+///
+public sealed class BuyerJobCondition : ListingCondition
+{
+ ///
+ /// A whitelist of jobs prototypes that can purchase this listing. Only one needs to be found.
+ ///
+ [DataField("whitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet? Whitelist;
+
+ ///
+ /// A blacklist of job prototypes that can purchase this listing. Only one needs to be found.
+ ///
+ [DataField("blacklist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet? Blacklist;
+
+ public override bool Condition(ListingConditionArgs args)
+ {
+ var ent = args.EntityManager;
+
+ if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null)
+ return true; //this is for things like surplus crate
+
+ if (Blacklist != null)
+ {
+ foreach (var role in mind.Mind.AllRoles)
+ {
+ if (role is not Job job)
+ continue;
+
+ if (Blacklist.Contains(job.Prototype.ID))
+ return false;
+ }
+ }
+
+ if (Whitelist != null)
+ {
+ var found = false;
+ foreach (var role in mind.Mind.AllRoles)
+ {
+ if (role is not Job job)
+ continue;
+
+ if (Whitelist.Contains(job.Prototype.ID))
+ found = true;
+ }
+ if (!found)
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs
new file mode 100644
index 0000000000..e9ca245ccd
--- /dev/null
+++ b/Content.Server/Store/Conditions/BuyerWhitelistCondition.cs
@@ -0,0 +1,41 @@
+using Content.Shared.Store;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Store.Conditions;
+
+///
+/// Filters out an entry based on the components or tags on an entity.
+///
+public sealed class BuyerWhitelistCondition : ListingCondition
+{
+ ///
+ /// A whitelist of tags or components.
+ ///
+ [DataField("whitelist")]
+ public EntityWhitelist? Whitelist;
+
+ ///
+ /// A blacklist of tags or components.
+ ///
+ [DataField("blacklist")]
+ public EntityWhitelist? Blacklist;
+
+ public override bool Condition(ListingConditionArgs args)
+ {
+ var ent = args.EntityManager;
+
+ if (Whitelist != null)
+ {
+ if (!Whitelist.IsValid(args.Buyer, ent))
+ return false;
+ }
+
+ if (Blacklist != null)
+ {
+ if (Blacklist.IsValid(args.Buyer, ent))
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Server/Store/Conditions/ListingLimitedStockCondition.cs b/Content.Server/Store/Conditions/ListingLimitedStockCondition.cs
new file mode 100644
index 0000000000..e1fdbfe892
--- /dev/null
+++ b/Content.Server/Store/Conditions/ListingLimitedStockCondition.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Store;
+
+namespace Content.Server.Store.Conditions;
+
+///
+/// Only allows a listing to be purchased a certain amount of times.
+///
+public sealed class ListingLimitedStockCondition : ListingCondition
+{
+ ///
+ /// The amount of times this listing can be purchased.
+ ///
+ [DataField("stock", required: true)]
+ public int Stock;
+
+ public override bool Condition(ListingConditionArgs args)
+ {
+ return args.Listing.PurchaseAmount < Stock;
+ }
+}
diff --git a/Content.Server/Store/Conditions/StoreWhitelistCondition.cs b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs
new file mode 100644
index 0000000000..ccef958320
--- /dev/null
+++ b/Content.Server/Store/Conditions/StoreWhitelistCondition.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Store;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Store.Conditions;
+
+///
+/// Filters out an entry based on the components or tags on the store itself.
+///
+public sealed class StoreWhitelistCondition : ListingCondition
+{
+ ///
+ /// A whitelist of tags or components.
+ ///
+ [DataField("whitelist")]
+ public EntityWhitelist? Whitelist;
+
+ ///
+ /// A blacklist of tags or components.
+ ///
+ [DataField("blacklist")]
+ public EntityWhitelist? Blacklist;
+
+ public override bool Condition(ListingConditionArgs args)
+ {
+ if (args.StoreEntity == null)
+ return false;
+
+ var ent = args.EntityManager;
+
+ if (Whitelist != null)
+ {
+ if (!Whitelist.IsValid(args.StoreEntity.Value, ent))
+ return false;
+ }
+
+ if (Blacklist != null)
+ {
+ if (Blacklist.IsValid(args.StoreEntity.Value, ent))
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Server/Store/Systems/StoreSystem.Listings.cs b/Content.Server/Store/Systems/StoreSystem.Listings.cs
new file mode 100644
index 0000000000..36b1a4b8c5
--- /dev/null
+++ b/Content.Server/Store/Systems/StoreSystem.Listings.cs
@@ -0,0 +1,127 @@
+using Content.Server.Store.Components;
+using Content.Shared.Store;
+
+namespace Content.Server.Store.Systems;
+
+public sealed partial class StoreSystem : EntitySystem
+{
+ ///
+ /// Refreshes all listings on a store.
+ /// Do not use if you don't know what you're doing.
+ ///
+ /// The store to refresh
+ public void RefreshAllListings(StoreComponent component)
+ {
+ component.Listings = GetAllListings();
+ }
+
+ ///
+ /// Gets all listings from a prototype.
+ ///
+ /// All the listings
+ public HashSet GetAllListings()
+ {
+ var allListings = _proto.EnumeratePrototypes();
+
+ var allData = new HashSet();
+
+ foreach (var listing in allListings)
+ allData.Add(listing);
+
+ return allData;
+ }
+
+ ///
+ /// Adds a listing from an Id to a store
+ ///
+ /// The store to add the listing to
+ /// The id of the listing
+ /// Whetehr or not the listing was added successfully
+ public bool TryAddListing(StoreComponent component, string listingId)
+ {
+ if (!_proto.TryIndex(listingId, out var proto))
+ {
+ Logger.Error("Attempted to add invalid listing.");
+ return false;
+ }
+ return TryAddListing(component, proto);
+ }
+
+ ///
+ /// Adds a listing to a store
+ ///
+ /// The store to add the listing to
+ /// The listing
+ /// Whether or not the listing was add successfully
+ public bool TryAddListing(StoreComponent component, ListingData listing)
+ {
+ return component.Listings.Add(listing);
+ }
+
+ ///
+ /// Gets the available listings for a store
+ ///
+ /// The person getting the listings.
+ /// The store the listings are coming from.
+ /// The available listings.
+ public IEnumerable GetAvailableListings(EntityUid user, StoreComponent component)
+ {
+ return GetAvailableListings(user, component.Listings, component.Categories, component.Owner);
+ }
+
+ ///
+ /// Gets the available listings for a user given an overall set of listings and categories to filter by.
+ ///
+ /// The person getting the listings.
+ /// All of the listings that are available. If null, will just get all listings from the prototypes.
+ /// What categories to filter by.
+ /// The physial entity of the store. Can be null.
+ /// The available listings.
+ public IEnumerable GetAvailableListings(EntityUid user, HashSet? listings, HashSet categories, EntityUid? storeEntity = null)
+ {
+ if (listings == null)
+ listings = GetAllListings();
+
+ foreach (var listing in listings)
+ {
+ if (!ListingHasCategory(listing, categories))
+ continue;
+
+ if (listing.Conditions != null)
+ {
+ var args = new ListingConditionArgs(user, storeEntity, listing, EntityManager);
+ var conditionsMet = true;
+
+ foreach (var condition in listing.Conditions)
+ {
+ if (!condition.Condition(args))
+ {
+ conditionsMet = false;
+ break;
+ }
+ }
+
+ if (!conditionsMet)
+ continue;
+ }
+
+ yield return listing;
+ }
+ }
+
+ ///
+ /// Checks if a listing appears in a list of given categories
+ ///
+ /// The listing itself.
+ /// The categories to check through.
+ /// If the listing was present in one of the categories.
+ public bool ListingHasCategory(ListingData listing, HashSet categories)
+ {
+ foreach (var cat in categories)
+ {
+ if (listing.Categories.Contains(cat))
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs
new file mode 100644
index 0000000000..b8514deda2
--- /dev/null
+++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs
@@ -0,0 +1,226 @@
+using Content.Server.Actions;
+using Content.Server.Administration.Logs;
+using Content.Server.Mind.Components;
+using Content.Server.Store.Components;
+using Content.Server.UserInterface;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Store;
+using Content.Shared.Database;
+using Robust.Server.GameObjects;
+using System.Linq;
+using Content.Server.Stack;
+using Content.Shared.Prototypes;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Store.Systems;
+
+public sealed partial class StoreSystem : EntitySystem
+{
+ [Dependency] private readonly IAdminLogManager _admin = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly ActionsSystem _actions = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly StackSystem _stack = default!;
+
+ private void InitializeUi()
+ {
+ SubscribeLocalEvent((_,c,r) => UpdateUserInterface(r.CurrentBuyer, c));
+ SubscribeLocalEvent(OnBuyRequest);
+ SubscribeLocalEvent(OnRequestWithdraw);
+ }
+
+ ///
+ /// Toggles the store Ui open and closed
+ ///
+ /// the person doing the toggling
+ /// the store being toggled
+ public void ToggleUi(EntityUid user, StoreComponent component)
+ {
+ if (!TryComp(user, out var actor))
+ return;
+
+ var ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
+ ui?.Toggle(actor.PlayerSession);
+
+ UpdateUserInterface(user, component, ui);
+ }
+
+ ///
+ /// Updates the user interface for a store and refreshes the listings
+ ///
+ /// The person who if opening the store ui. Listings are filtered based on this.
+ /// The store component being refreshed.
+ ///
+ public void UpdateUserInterface(EntityUid? user, StoreComponent component, BoundUserInterface? ui = null)
+ {
+ if (ui == null)
+ {
+ ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
+ if (ui == null)
+ {
+ Logger.Error("No Ui key.");
+ return;
+ }
+ }
+
+ //if we haven't opened it before, initialize the shit
+ if (!component.Opened)
+ {
+ InitializeFromPreset(component.Preset, component);
+ component.Opened = true;
+ }
+
+ //this is the person who will be passed into logic for all listing filtering.
+ var buyer = user;
+ if (buyer != null) //if we have no "buyer" for this update, then don't update the listings
+ {
+ if (component.AccountOwner != null) //if we have one stored, then use that instead
+ buyer = component.AccountOwner.Value;
+
+ component.LastAvailableListings = GetAvailableListings(buyer.Value, component).ToHashSet();
+ }
+
+ //dictionary for all currencies, including 0 values for currencies on the whitelist
+ Dictionary allCurrency = new();
+ foreach (var supported in component.CurrencyWhitelist)
+ {
+ allCurrency.Add(supported, FixedPoint2.Zero);
+
+ if (component.Balance.ContainsKey(supported))
+ allCurrency[supported] = component.Balance[supported];
+ }
+
+ var state = new StoreUpdateState(buyer, component.LastAvailableListings, allCurrency);
+ ui.SetState(state);
+ }
+
+ ///
+ /// Handles whenever a purchase was made.
+ ///
+ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListingMessage msg)
+ {
+ ListingData? listing = component.Listings.FirstOrDefault(x => x.Equals(msg.Listing));
+ if (listing == null) //make sure this listing actually exists
+ {
+ Logger.Debug("listing does not exist");
+ return;
+ }
+
+ //verify that we can actually buy this listing and it wasn't added
+ if (!ListingHasCategory(listing, component.Categories))
+ return;
+ //condition checking because why not
+ if (listing.Conditions != null)
+ {
+ var args = new ListingConditionArgs(msg.Buyer, component.Owner, listing, EntityManager);
+ var conditionsMet = true;
+
+ foreach (var condition in listing.Conditions.Where(condition => !condition.Condition(args)))
+ conditionsMet = false;
+
+ if (!conditionsMet)
+ return;
+ }
+
+ //check that we have enough money
+ foreach (var currency in listing.Cost)
+ {
+ if (!component.Balance.TryGetValue(currency.Key, out var balance) || balance < currency.Value)
+ {
+ _audio.Play(component.InsufficientFundsSound, Filter.SinglePlayer(msg.Session), uid);
+ return;
+ }
+ }
+ //subtract the cash
+ foreach (var currency in listing.Cost)
+ component.Balance[currency.Key] -= currency.Value;
+
+ //spawn entity
+ if (listing.ProductEntity != null)
+ {
+ var product = Spawn(listing.ProductEntity, Transform(msg.Buyer).Coordinates);
+ _hands.TryPickupAnyHand(msg.Buyer, product);
+ }
+
+ //give action
+ if (listing.ProductAction != null)
+ {
+ var action = new InstantAction(_proto.Index(listing.ProductAction));
+ _actions.AddAction(msg.Buyer, action, null);
+ }
+
+ //broadcast event
+ if (listing.ProductEvent != null)
+ {
+ RaiseLocalEvent(listing.ProductEvent);
+ }
+
+ //log dat shit.
+ if (TryComp(msg.Buyer, out var mind))
+ {
+ _admin.Add(LogType.StorePurchase, LogImpact.Low,
+ $"{ToPrettyString(mind.Owner):player} purchased listing \"{listing.Name}\" from {ToPrettyString(uid)}");
+ }
+
+ listing.PurchaseAmount++; //track how many times something has been purchased
+ _audio.Play(component.BuySuccessSound, Filter.SinglePlayer(msg.Session), uid); //cha-ching!
+
+ UpdateUserInterface(msg.Buyer, component);
+ }
+
+ ///
+ /// Handles dispensing the currency you requested to be withdrawn.
+ ///
+ ///
+ /// This would need to be done should a currency with decimal values need to use it.
+ /// not quite sure how to handle that
+ ///
+ private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreRequestWithdrawMessage msg)
+ {
+ //make sure we have enough cash in the bank and we actually support this currency
+ if (!component.Balance.TryGetValue(msg.Currency, out var currentAmount) || currentAmount < msg.Amount)
+ return;
+
+ //make sure a malicious client didn't send us random shit
+ if (!_proto.TryIndex(msg.Currency, out var proto))
+ return;
+
+ //we need an actually valid entity to spawn. This check has been done earlier, but just in case.
+ if (proto.EntityId == null || !proto.CanWithdraw)
+ return;
+
+ var entproto = _proto.Index(proto.EntityId);
+
+ var amountRemaining = msg.Amount;
+ var coordinates = Transform(msg.Buyer).Coordinates;
+ if (entproto.HasComponent())
+ {
+ while (amountRemaining > 0)
+ {
+ var ent = Spawn(proto.EntityId, coordinates);
+ var stackComponent = Comp(ent); //we already know it exists
+
+ var amountPerStack = Math.Min(stackComponent.MaxCount, amountRemaining);
+
+ _stack.SetCount(ent, amountPerStack, stackComponent);
+ amountRemaining -= amountPerStack;
+ _hands.TryPickupAnyHand(msg.Buyer, ent);
+ }
+ }
+ else //please for the love of christ give your currency stack component
+ {
+ while (amountRemaining > 0)
+ {
+ var ent = Spawn(proto.EntityId, coordinates);
+ _hands.TryPickupAnyHand(msg.Buyer, ent);
+ amountRemaining--;
+ }
+ }
+
+ component.Balance[msg.Currency] -= msg.Amount;
+ UpdateUserInterface(msg.Buyer, component);
+ }
+}
diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs
new file mode 100644
index 0000000000..dfa2c16dfc
--- /dev/null
+++ b/Content.Server/Store/Systems/StoreSystem.cs
@@ -0,0 +1,154 @@
+using Content.Server.Stack;
+using Content.Server.Store.Components;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Store;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using System.Linq;
+using Content.Server.UserInterface;
+
+namespace Content.Server.Store.Systems;
+
+///
+/// Manages general interactions with a store and different entities,
+/// getting listings for stores, and interfacing with the store UI.
+///
+public sealed partial class StoreSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent((_,c,a) => UpdateUserInterface(a.User, c));
+
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+
+ InitializeUi();
+ }
+
+ private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args)
+ {
+ RaiseLocalEvent(uid, new StoreAddedEvent(), true);
+ }
+
+ private void OnShutdown(EntityUid uid, StoreComponent component, ComponentShutdown args)
+ {
+ RaiseLocalEvent(uid, new StoreRemovedEvent(), true);
+ }
+
+ private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args)
+ {
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (args.Target == null || !TryComp(args.Target, out var store))
+ return;
+
+ //if you somehow are inserting cash before the store initializes.
+ if (!store.Opened)
+ {
+ InitializeFromPreset(store.Preset, store);
+ store.Opened = true;
+ }
+
+ args.Handled = TryAddCurrency(GetCurrencyValue(component), store);
+
+ if (args.Handled)
+ {
+ var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
+ _popup.PopupEntity(msg, args.Target.Value, Filter.Pvs(args.Target.Value));
+ QueueDel(args.Used);
+ }
+ }
+
+ ///
+ /// Gets the value from an entity's currency component.
+ /// Scales with stacks.
+ ///
+ ///
+ /// The value of the currency
+ public Dictionary GetCurrencyValue(CurrencyComponent component)
+ {
+ TryComp(component.Owner, out var stack);
+ var amount = stack?.Count ?? 1;
+
+ return component.Price.ToDictionary(v => v.Key, p => p.Value * amount);
+ }
+
+ ///
+ /// Tries to add a currency to a store's balance.
+ ///
+ /// The currency to add
+ /// The store to add it to
+ /// Whether or not the currency was succesfully added
+ public bool TryAddCurrency(CurrencyComponent component, StoreComponent store)
+ {
+ return TryAddCurrency(GetCurrencyValue(component), store);
+ }
+
+ ///
+ /// Tries to add a currency to a store's balance
+ ///
+ /// The value to add to the store
+ /// The store to add it to
+ /// Whether or not the currency was succesfully added
+ public bool TryAddCurrency(Dictionary currency, StoreComponent store)
+ {
+ //verify these before values are modified
+ foreach (var type in currency)
+ {
+ if (!store.CurrencyWhitelist.Contains(type.Key))
+ return false;
+ }
+
+ foreach (var type in currency)
+ {
+ if (!store.Balance.TryAdd(type.Key, type.Value))
+ store.Balance[type.Key] += type.Value;
+ }
+
+ UpdateUserInterface(null, store);
+ return true;
+ }
+
+ ///
+ /// Initializes a store based on a preset ID
+ ///
+ /// The ID of a store preset prototype
+ /// The store being initialized
+ public void InitializeFromPreset(string? preset, StoreComponent component)
+ {
+ if (preset == null)
+ return;
+
+ if (!_proto.TryIndex(preset, out var proto))
+ return;
+
+ InitializeFromPreset(proto, component);
+ }
+
+ ///
+ /// Initializes a store based on a given preset
+ ///
+ /// The StorePresetPrototype
+ /// The store being initialized
+ public void InitializeFromPreset(StorePresetPrototype preset, StoreComponent component)
+ {
+ RefreshAllListings(component);
+ component.Preset = preset.ID;
+ component.CurrencyWhitelist.UnionWith(preset.CurrencyWhitelist);
+ component.Categories.UnionWith(preset.Categories);
+ if (component.Balance == new Dictionary() && preset.InitialBalance != null) //if we don't have a value stored, use the preset
+ TryAddCurrency(preset.InitialBalance, component);
+
+ var ui = component.Owner.GetUIOrNull(StoreUiKey.Key);
+ ui?.SetState(new StoreInitializeState(preset.StoreName));
+ }
+}
diff --git a/Content.Server/Traitor/Uplink/Account/UplinkAccountEvents.cs b/Content.Server/Traitor/Uplink/Account/UplinkAccountEvents.cs
deleted file mode 100644
index 0345df2fac..0000000000
--- a/Content.Server/Traitor/Uplink/Account/UplinkAccountEvents.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Content.Shared.Traitor.Uplink;
-
-namespace Content.Server.Traitor.Uplink.Account
-{
- ///
- /// Invokes when one of the UplinkAccounts changed its TC balance
- ///
- public sealed class UplinkAccountBalanceChanged : EntityEventArgs
- {
- public readonly UplinkAccount Account;
-
- ///
- /// Difference between NewBalance - OldBalance
- ///
- public readonly int Difference;
-
- public readonly int NewBalance;
- public readonly int OldBalance;
-
- public UplinkAccountBalanceChanged(UplinkAccount account, int difference)
- {
- Account = account;
- Difference = difference;
-
- NewBalance = account.Balance;
- OldBalance = account.Balance - difference;
-
- }
- }
-}
diff --git a/Content.Server/Traitor/Uplink/Account/UplinkAccountsSystem.cs b/Content.Server/Traitor/Uplink/Account/UplinkAccountsSystem.cs
deleted file mode 100644
index d3ad87dd25..0000000000
--- a/Content.Server/Traitor/Uplink/Account/UplinkAccountsSystem.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Shared.Stacks;
-using Content.Shared.Traitor.Uplink;
-using Robust.Shared.Map;
-
-namespace Content.Server.Traitor.Uplink.Account
-{
- ///
- /// Manage all registred uplink accounts and their balance
- ///
- public sealed class UplinkAccountsSystem : EntitySystem
- {
- public const string TelecrystalProtoId = "Telecrystal";
-
- [Dependency]
- private readonly UplinkListingSytem _listingSystem = default!;
- [Dependency]
- private readonly SharedStackSystem _stackSystem = default!;
-
- private readonly HashSet _accounts = new();
-
- public bool AddNewAccount(UplinkAccount acc)
- {
- return _accounts.Add(acc);
- }
-
- public bool HasAccount(EntityUid holder) =>
- _accounts.Any(acct => acct.AccountHolder == holder);
-
- ///
- /// Add TC to uplinks account balance
- ///
- public bool AddToBalance(UplinkAccount account, int toAdd)
- {
- account.Balance += toAdd;
-
- RaiseLocalEvent(new UplinkAccountBalanceChanged(account, toAdd));
- return true;
- }
-
- ///
- /// Charge TC from uplinks account balance
- ///
- public bool RemoveFromBalance(UplinkAccount account, int price)
- {
- if (account.Balance - price < 0)
- return false;
-
- account.Balance -= price;
-
- RaiseLocalEvent(new UplinkAccountBalanceChanged(account, -price));
- return true;
- }
-
- ///
- /// Force-set TC uplinks account balance to a new value
- ///
- public bool SetBalance(UplinkAccount account, int newBalance)
- {
- if (newBalance < 0)
- return false;
-
- var dif = newBalance - account.Balance;
- account.Balance = newBalance;
- RaiseLocalEvent(new UplinkAccountBalanceChanged(account, dif));
- return true;
-
- }
-
- public bool TryPurchaseItem(UplinkAccount acc, string itemId, EntityCoordinates spawnCoords, [NotNullWhen(true)] out EntityUid? purchasedItem)
- {
- purchasedItem = null;
-
- if (!_listingSystem.TryGetListing(itemId, out var listing))
- return false;
-
- if (acc.Balance < listing.Price)
- return false;
-
- if (!RemoveFromBalance(acc, listing.Price))
- return false;
-
- purchasedItem = EntityManager.SpawnEntity(listing.ItemId, spawnCoords);
- return true;
- }
-
- public bool TryWithdrawTC(UplinkAccount acc, int tc, EntityCoordinates spawnCoords, [NotNullWhen(true)] out EntityUid? stackUid)
- {
- stackUid = null;
-
- // try to charge TC from players account
- var actTC = Math.Min(tc, acc.Balance);
- if (actTC <= 0)
- return false;
- if (!RemoveFromBalance(acc, actTC))
- return false;
-
- // create a stack of TCs near player
- var stackEntity = EntityManager.SpawnEntity(TelecrystalProtoId, spawnCoords);
- stackUid = stackEntity;
-
- // set right amount in stack
- _stackSystem.SetCount(stackUid.Value, actTC);
- return true;
- }
- }
-}
diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs
index 156a13e465..5145c2d99d 100644
--- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs
+++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs
@@ -1,8 +1,7 @@
using Content.Server.Administration;
-using Content.Server.Traitor.Uplink.Account;
using Content.Shared.Administration;
using Content.Shared.CCVar;
-using Content.Shared.Traitor.Uplink;
+using Content.Shared.FixedPoint;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
@@ -82,15 +81,10 @@ namespace Content.Server.Traitor.Uplink.Commands
// Get TC count
var configManager = IoCManager.Resolve();
var tcCount = configManager.GetCVar(CCVars.TraitorStartingBalance);
-
- // Get account
- var uplinkAccount = new UplinkAccount(tcCount, user);
- var accounts = entityManager.EntitySysManager.GetEntitySystem();
- accounts.AddNewAccount(uplinkAccount);
-
+ Logger.Debug(entityManager.ToPrettyString(user));
// Finally add uplink
- if (!entityManager.EntitySysManager.GetEntitySystem()
- .AddUplink(user, uplinkAccount, uplinkEntity))
+ var uplinkSys = entityManager.EntitySysManager.GetEntitySystem();
+ if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
{
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
return;
diff --git a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs
index 5682863f2f..8258034e67 100644
--- a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs
+++ b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleComponent.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Store;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
namespace Content.Server.Traitor.Uplink.SurplusBundle;
///
@@ -12,4 +15,11 @@ public sealed class SurplusBundleComponent : Component
[ViewVariables(VVAccess.ReadOnly)]
[DataField("totalPrice")]
public int TotalPrice = 20;
+
+ ///
+ /// The preset that will be used to get all the listings.
+ /// Currently just defaults to the basic uplink.
+ ///
+ [DataField("storePreset", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string StorePreset = "StorePresetUplink";
}
diff --git a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs
index fa704aef22..11187e969f 100644
--- a/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs
+++ b/Content.Server/Traitor/Uplink/SurplusBundle/SurplusBundleSystem.cs
@@ -1,7 +1,8 @@
using System.Linq;
-using Content.Server.Storage.Components;
+using Content.Server.Store.Systems;
using Content.Server.Storage.EntitySystems;
-using Content.Shared.PDA;
+using Content.Shared.Store;
+using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -12,23 +13,25 @@ public sealed class SurplusBundleSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
+ [Dependency] private readonly StoreSystem _store = default!;
- private UplinkStoreListingPrototype[] _uplinks = default!;
+ private ListingData[] _listings = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnMapInit);
- InitList();
+ SubscribeLocalEvent(OnInit);
}
- private void InitList()
+ private void OnInit(EntityUid uid, SurplusBundleComponent component, ComponentInit args)
{
- // sort data in price descending order
- _uplinks = _prototypeManager.EnumeratePrototypes()
- .Where(item => item.CanSurplus).ToArray();
- Array.Sort(_uplinks, (a, b) => b.Price - a.Price);
+ var storePreset = _prototypeManager.Index(component.StorePreset);
+
+ _listings = _store.GetAvailableListings(uid, null, storePreset.Categories).ToArray();
+
+ Array.Sort(_listings, (a, b) => (int) (b.Cost.Values.Sum() - a.Cost.Values.Sum())); //this might get weird with multicurrency but don't think about it
}
private void OnMapInit(EntityUid uid, SurplusBundleComponent component, MapInitEvent args)
@@ -46,19 +49,19 @@ public sealed class SurplusBundleSystem : EntitySystem
var content = GetRandomContent(component.TotalPrice);
foreach (var item in content)
{
- var ent = EntityManager.SpawnEntity(item.ItemId, cords);
+ var ent = EntityManager.SpawnEntity(item.ProductEntity, cords);
_entityStorage.Insert(ent, component.Owner);
}
}
// wow, is this leetcode reference?
- private List GetRandomContent(int targetCost)
+ private List GetRandomContent(FixedPoint2 targetCost)
{
- var ret = new List();
- if (_uplinks.Length == 0)
+ var ret = new List();
+ if (_listings.Length == 0)
return ret;
- var totalCost = 0;
+ var totalCost = FixedPoint2.Zero;
var index = 0;
while (totalCost < targetCost)
{
@@ -66,10 +69,10 @@ public sealed class SurplusBundleSystem : EntitySystem
// Find new item with the lowest acceptable price
// All expansive items will be before index, all acceptable after
var remainingBudget = targetCost - totalCost;
- while (_uplinks[index].Price > remainingBudget)
+ while (_listings[index].Cost.Values.Sum() > remainingBudget)
{
index++;
- if (index >= _uplinks.Length)
+ if (index >= _listings.Length)
{
// Looks like no cheap items left
// It shouldn't be case for ss14 content
@@ -79,10 +82,10 @@ public sealed class SurplusBundleSystem : EntitySystem
}
// Select random listing and add into crate
- var randomIndex = _random.Next(index, _uplinks.Length);
- var randomItem = _uplinks[randomIndex];
+ var randomIndex = _random.Next(index, _listings.Length);
+ var randomItem = _listings[randomIndex];
ret.Add(randomItem);
- totalCost += randomItem.Price;
+ totalCost += randomItem.Cost.Values.Sum();
}
return ret;
diff --git a/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalComponent.cs b/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalComponent.cs
deleted file mode 100644
index 9b3a3636b8..0000000000
--- a/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Traitor.Uplink.Telecrystal
-{
- [RegisterComponent]
- public sealed class TelecrystalComponent : Component
- {
- }
-}
diff --git a/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalSystem.cs b/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalSystem.cs
deleted file mode 100644
index cf98452338..0000000000
--- a/Content.Server/Traitor/Uplink/Telecrystal/TelecrystalSystem.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using Content.Server.Traitor.Uplink.Account;
-using Content.Server.Traitor.Uplink.Components;
-using Content.Shared.Interaction;
-using Content.Shared.Popups;
-using Content.Shared.Stacks;
-
-namespace Content.Server.Traitor.Uplink.Telecrystal
-{
- public sealed class TelecrystalSystem : EntitySystem
- {
- [Dependency]
- private readonly UplinkAccountsSystem _accounts = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnAfterInteract);
- }
-
- private void OnAfterInteract(EntityUid uid, TelecrystalComponent component, AfterInteractEvent args)
- {
- if (args.Handled || !args.CanReach)
- return;
-
- if (args.Target == null || !EntityManager.TryGetComponent(args.Target.Value, out UplinkComponent? uplink))
- return;
-
- // TODO: when uplink will have some auth logic (like PDA ringtone code)
- // check if uplink open before adding TC
- // No metagaming by using this on every PDA around just to see if it gets used up.
-
- var acc = uplink.UplinkAccount;
- if (acc == null)
- return;
-
- EntityManager.TryGetComponent(uid, out SharedStackComponent? stack);
-
- var tcCount = stack != null ? stack.Count : 1;
- if (!_accounts.AddToBalance(acc, tcCount))
- return;
-
- var msg = Loc.GetString("telecrystal-component-sucs-inserted",
- ("source", args.Used), ("target", args.Target));
-
- args.User.PopupMessage(args.User, msg);
-
- EntityManager.DeleteEntity(uid);
-
- args.Handled = true;
- }
- }
-}
diff --git a/Content.Server/Traitor/Uplink/UplinkComponent.cs b/Content.Server/Traitor/Uplink/UplinkComponent.cs
deleted file mode 100644
index efa1afd925..0000000000
--- a/Content.Server/Traitor/Uplink/UplinkComponent.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.Roles;
-using Content.Shared.Traitor.Uplink;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
-
-namespace Content.Server.Traitor.Uplink.Components
-{
- [RegisterComponent]
- public sealed class UplinkComponent : Component
- {
- [ViewVariables]
- [DataField("buySuccessSound")]
- public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg");
-
- [ViewVariables]
- [DataField("insufficientFundsSound")]
- public SoundSpecifier InsufficientFundsSound = new SoundPathSpecifier("/Audio/Effects/error.ogg");
-
- [DataField("activatesInHands")]
- public bool ActivatesInHands = false;
-
- [DataField("presetInfo")]
- public PresetUplinkInfo? PresetInfo = null;
-
- [ViewVariables] public UplinkAccount? UplinkAccount;
-
- [ViewVariables, DataField("jobWhiteList", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))]
- public HashSet? JobWhitelist = null;
-
- [Serializable]
- [DataDefinition]
- public sealed class PresetUplinkInfo
- {
- [DataField("balance")]
- public int StartingBalance;
- }
- }
-}
diff --git a/Content.Server/Traitor/Uplink/UplinkEvents.cs b/Content.Server/Traitor/Uplink/UplinkEvents.cs
deleted file mode 100644
index 6de247a59a..0000000000
--- a/Content.Server/Traitor/Uplink/UplinkEvents.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Server.Traitor.Uplink.Components;
-
-namespace Content.Server.Traitor.Uplink
-{
- public sealed class UplinkInitEvent : EntityEventArgs
- {
- public UplinkComponent Uplink;
-
- public UplinkInitEvent(UplinkComponent uplink)
- {
- Uplink = uplink;
- }
- }
-
- public sealed class UplinkRemovedEvent : EntityEventArgs
- {
- }
-}
diff --git a/Content.Server/Traitor/Uplink/UplinkListingSytem.cs b/Content.Server/Traitor/Uplink/UplinkListingSytem.cs
deleted file mode 100644
index 67458ad5f8..0000000000
--- a/Content.Server/Traitor/Uplink/UplinkListingSytem.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Content.Shared.PDA;
-using Content.Shared.Traitor.Uplink;
-using Robust.Shared.Prototypes;
-using System.Diagnostics.CodeAnalysis;
-
-namespace Content.Server.Traitor.Uplink
-{
- ///
- /// Contains and controls all items in traitors uplink shop
- ///
- public sealed class UplinkListingSytem : EntitySystem
- {
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
- private readonly Dictionary _listings = new();
-
- public override void Initialize()
- {
- base.Initialize();
-
- foreach (var item in _prototypeManager.EnumeratePrototypes())
- {
- var newListing = new UplinkListingData(item.ListingName, item.ItemId,
- item.Price, item.Category, item.Description, item.Icon, item.JobWhitelist);
-
- RegisterUplinkListing(newListing);
- }
- }
-
- private void RegisterUplinkListing(UplinkListingData listing)
- {
- if (!ContainsListing(listing))
- {
- _listings.Add(listing.ItemId, listing);
- }
- }
-
- public bool ContainsListing(UplinkListingData listing)
- {
- return _listings.ContainsKey(listing.ItemId);
- }
-
- public bool TryGetListing(string itemID, [NotNullWhen(true)] out UplinkListingData? data)
- {
- return _listings.TryGetValue(itemID, out data);
- }
-
- public IReadOnlyDictionary GetListings()
- {
- return _listings;
- }
- }
-}
diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs
index 6f8da4f627..0a5968aa7a 100644
--- a/Content.Server/Traitor/Uplink/UplinkSystem.cs
+++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs
@@ -1,230 +1,41 @@
-using System.Linq;
-using Content.Server.Mind.Components;
-using Content.Server.Roles;
-using Content.Server.Traitor.Uplink.Account;
-using Content.Server.Traitor.Uplink.Components;
-using Content.Server.UserInterface;
+using Content.Server.Store.Systems;
using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.PDA;
-using Content.Shared.Traitor.Uplink;
-using Robust.Server.GameObjects;
-using Robust.Server.Player;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
+using Content.Server.Store.Components;
+using Content.Shared.FixedPoint;
namespace Content.Server.Traitor.Uplink
{
public sealed class UplinkSystem : EntitySystem
{
- [Dependency]
- private readonly UplinkAccountsSystem _accounts = default!;
- [Dependency]
- private readonly UplinkListingSytem _listing = default!;
-
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+ [Dependency] private readonly StoreSystem _store = default!;
- public override void Initialize()
+ public const string TelecrystalCurrencyPrototype = "Telecrystal";
+
+ ///
+ /// Gets the amount of TC on an "uplink"
+ /// Mostly just here for legacy systems based on uplink.
+ ///
+ ///
+ /// the amount of TC
+ public int GetTCBalance(StoreComponent component)
{
- base.Initialize();
-
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnRemove);
- SubscribeLocalEvent(OnActivate);
-
- // UI events
- SubscribeLocalEvent(OnBuy);
- SubscribeLocalEvent(OnRequestUpdateUI);
- SubscribeLocalEvent(OnWithdrawTC);
-
- SubscribeLocalEvent(OnBalanceChangedBroadcast);
+ FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype);
+ return tcBalance != null ? tcBalance.Value.Int() : 0;
}
- public void SetAccount(UplinkComponent component, UplinkAccount account)
- {
- if (component.UplinkAccount != null)
- {
- Logger.Error("Can't init one uplink with different account!");
- return;
- }
-
- component.UplinkAccount = account;
- }
-
- private void OnInit(EntityUid uid, UplinkComponent component, ComponentInit args)
- {
- RaiseLocalEvent(uid, new UplinkInitEvent(component), true);
-
- // if component has a preset info (probably spawn by admin)
- // create a new account and register it for this uplink
- if (component.PresetInfo != null)
- {
- var account = new UplinkAccount(component.PresetInfo.StartingBalance);
- _accounts.AddNewAccount(account);
- SetAccount(component, account);
- }
- }
-
- private void OnRemove(EntityUid uid, UplinkComponent component, ComponentRemove args)
- {
- RaiseLocalEvent(uid, new UplinkRemovedEvent(), true);
- }
-
- private void OnActivate(EntityUid uid, UplinkComponent component, ActivateInWorldEvent args)
- {
- if (args.Handled)
- return;
-
- // check if uplinks activates directly or use some proxy, like a PDA
- if (!component.ActivatesInHands)
- return;
- if (component.UplinkAccount == null)
- return;
-
- if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
- return;
-
- ToggleUplinkUI(component, actor.PlayerSession);
- args.Handled = true;
- }
-
- private void OnBalanceChangedBroadcast(UplinkAccountBalanceChanged ev)
- {
- foreach (var uplink in EntityManager.EntityQuery())
- {
- if (uplink.UplinkAccount == ev.Account)
- {
- UpdateUserInterface(uplink);
- }
- }
- }
-
- private void OnRequestUpdateUI(EntityUid uid, UplinkComponent uplink, UplinkRequestUpdateInterfaceMessage args)
- {
- UpdateUserInterface(uplink);
- }
-
- private void OnBuy(EntityUid uid, UplinkComponent uplink, UplinkBuyListingMessage message)
- {
- if (message.Session.AttachedEntity is not { Valid: true } player) return;
- if (uplink.UplinkAccount == null) return;
-
- if (!_accounts.TryPurchaseItem(uplink.UplinkAccount, message.ItemId,
- EntityManager.GetComponent(player).Coordinates, out var entity))
- {
- SoundSystem.Play(uplink.InsufficientFundsSound.GetSound(),
- Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default);
- RaiseNetworkEvent(new UplinkInsufficientFundsMessage(), message.Session.ConnectedClient);
- return;
- }
-
- _handsSystem.PickupOrDrop(player, entity.Value);
-
- SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
- Filter.SinglePlayer(message.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
-
- RaiseNetworkEvent(new UplinkBuySuccessMessage(), message.Session.ConnectedClient);
- }
-
- private void OnWithdrawTC(EntityUid uid, UplinkComponent uplink, UplinkTryWithdrawTC args)
- {
- var acc = uplink.UplinkAccount;
- if (acc == null)
- return;
-
- if (args.Session.AttachedEntity is not { Valid: true } player) return;
- var cords = EntityManager.GetComponent(player).Coordinates;
-
- // try to withdraw TCs from account
- if (!_accounts.TryWithdrawTC(acc, args.TC, cords, out var tcUid))
- return;
-
- // try to put it into players hands
- _handsSystem.PickupOrDrop(player, tcUid.Value);
-
- // play buying sound
- SoundSystem.Play(uplink.BuySuccessSound.GetSound(),
- Filter.SinglePlayer(args.Session), uplink.Owner, AudioParams.Default.WithVolume(-8f));
-
- UpdateUserInterface(uplink);
- }
-
- public void ToggleUplinkUI(UplinkComponent component, IPlayerSession session)
- {
- var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
- ui?.Toggle(session);
-
- UpdateUserInterface(component);
- }
-
- private void UpdateUserInterface(UplinkComponent component)
- {
- var ui = component.Owner.GetUIOrNull(UplinkUiKey.Key);
- if (ui == null)
- return;
-
- var listings = _listing.GetListings().Values.ToList();
- var acc = component.UplinkAccount;
-
- UplinkAccountData accData;
- if (acc != null)
- {
- // if we don't have a jobwhitelist stored, get a new one
- if (component.JobWhitelist == null &&
- acc.AccountHolder != null &&
- TryComp(acc.AccountHolder, out var mind) &&
- mind.Mind != null)
- {
- HashSet? jobList = new();
- foreach (var role in mind.Mind.AllRoles.ToList())
- {
- if (role.GetType() == typeof(Job))
- {
- var job = (Job) role;
- jobList.Add(job.Prototype.ID);
- }
- }
- component.JobWhitelist = jobList;
- }
-
- // filter out items not on the whitelist
- for (var i = 0; i < listings.Count; i++)
- {
- var entry = listings[i];
- if (entry.JobWhitelist != null)
- {
- var found = false;
- if (component.JobWhitelist != null)
- {
- foreach (var job in component.JobWhitelist)
- {
- if (entry.JobWhitelist.Contains(job))
- {
- found = true;
- break;
- }
- }
- }
- if (!found)
- {
- listings.Remove(entry);
- i--;
- }
- }
- }
- accData = new UplinkAccountData(acc.AccountHolder, acc.Balance);
- }
- else
- {
- accData = new UplinkAccountData(null, 0);
- }
-
- ui.SetState(new UplinkUpdateState(accData, listings.ToArray()));
- }
-
- public bool AddUplink(EntityUid user, UplinkAccount account, EntityUid? uplinkEntity = null)
+ ///
+ /// Adds an uplink to the target
+ ///
+ /// The person who is getting the uplink
+ /// The amount of currency on the uplink. If null, will just use the amount specified in the preset.
+ /// The id of the storepreset
+ /// The entity that will actually have the uplink functionality. Defaults to the PDA if null.
+ /// Whether or not the uplink was added successfully
+ public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null)
{
// Try to find target item
if (uplinkEntity == null)
@@ -234,11 +45,17 @@ namespace Content.Server.Traitor.Uplink
return false;
}
- var uplink = uplinkEntity.Value.EnsureComponent();
- SetAccount(uplink, account);
+ var store = EnsureComp(uplinkEntity.Value);
+ _store.InitializeFromPreset(uplinkPresetId, store);
+ store.AccountOwner = user;
+ store.Balance.Clear();
- if (!HasComp(uplinkEntity.Value))
- uplink.ActivatesInHands = true;
+ if (balance != null)
+ {
+ store.Balance.Clear();
+ _store.TryAddCurrency(
+ new Dictionary() { { TelecrystalCurrencyPrototype, balance.Value } }, store);
+ }
// TODO add BUI. Currently can't be done outside of yaml -_-
@@ -248,14 +65,13 @@ namespace Content.Server.Traitor.Uplink
private EntityUid? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
-
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
if (!pdaUid.ContainedEntity.HasValue) continue;
- if (HasComp(pdaUid.ContainedEntity.Value))
+ if (HasComp(pdaUid.ContainedEntity.Value) || HasComp(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
}
@@ -263,7 +79,7 @@ namespace Content.Server.Traitor.Uplink
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
- if (HasComp(item))
+ if (HasComp(item) || HasComp(item))
return item;
}
diff --git a/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs b/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs
index 3eb7912590..1c5908fe14 100644
--- a/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs
+++ b/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs
@@ -1,7 +1,9 @@
-using Content.Server.Mind.Components;
-using Content.Server.Traitor.Uplink.Account;
-using Content.Server.Traitor.Uplink.Components;
+using Content.Server.Mind.Components;
using Content.Server.TraitorDeathMatch.Components;
+using Content.Server.Store.Components;
+using Content.Server.Store.Systems;
+using Content.Server.Traitor.Uplink;
+using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Popups;
@@ -12,8 +14,11 @@ namespace Content.Server.TraitorDeathMatch;
public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
- [Dependency] private readonly UplinkAccountsSystem _uplink = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly UplinkSystem _uplink = default!;
+ [Dependency] private readonly StoreSystem _store = default!;
+
+ private const string TcCurrencyPrototype = "Telecrystal";
public override void Initialize()
{
@@ -43,7 +48,7 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return;
}
- if (!EntityManager.TryGetComponent(args.Used, out var victimUplink))
+ if (!EntityManager.TryGetComponent(args.Used, out var victimUplink))
{
_popup.PopupEntity(Loc.GetString(
"traitor-death-match-redemption-component-interact-using-main-message",
@@ -72,10 +77,10 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return;
}
- UplinkComponent? userUplink = null;
+ StoreComponent? userUplink = null;
if (_inventory.TryGetSlotEntity(args.User, "id", out var pdaUid) &&
- EntityManager.TryGetComponent(pdaUid, out var userUplinkComponent))
+ EntityManager.TryGetComponent(pdaUid, out var userUplinkComponent))
userUplink = userUplinkComponent;
if (userUplink == null)
@@ -88,35 +93,13 @@ public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
return;
}
+
// We have finally determined both PDA components. FINALLY.
- var userAccount = userUplink.UplinkAccount;
- var victimAccount = victimUplink.UplinkAccount;
-
- if (userAccount == null)
- {
- _popup.PopupEntity(Loc.GetString(
- "traitor-death-match-redemption-component-interact-using-main-message",
- ("secondMessage",
- Loc.GetString(
- "traitor-death-match-redemption-component-interact-using-user-no-uplink-account-message"))), uid, Filter.Entities(args.User));
- return;
- }
-
- if (victimAccount == null)
- {
- _popup.PopupEntity(Loc.GetString(
- "traitor-death-match-redemption-component-interact-using-main-message",
- ("secondMessage",
- Loc.GetString(
- "traitor-death-match-redemption-component-interact-using-victim-no-uplink-account-message"))), uid, Filter.Entities(args.User));
- return;
- }
-
- // 4 is the per-PDA bonus amount.
- var transferAmount = victimAccount.Balance + 4;
- _uplink.SetBalance(victimAccount, 0);
- _uplink.AddToBalance(userAccount, transferAmount);
+ // 4 is the per-PDA bonus amount
+ var transferAmount = _uplink.GetTCBalance(victimUplink) + 4;
+ victimUplink.Balance.Clear();
+ _store.TryAddCurrency(new Dictionary() { {"Telecrystal", FixedPoint2.New(transferAmount)}}, userUplink);
EntityManager.DeleteEntity(victimUplink.Owner);
diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs
index ca46b84a19..2373126830 100644
--- a/Content.Shared.Database/LogType.cs
+++ b/Content.Shared.Database/LogType.cs
@@ -75,4 +75,5 @@ public enum LogType
Gib = 70,
Identity = 71,
CableCut = 72,
+ StorePurchase = 73,
}
diff --git a/Content.Shared/PDA/UplinkCategory.cs b/Content.Shared/PDA/UplinkCategory.cs
deleted file mode 100644
index 94e5d82b44..0000000000
--- a/Content.Shared/PDA/UplinkCategory.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-
-namespace Content.Shared.PDA
-{
- public enum UplinkCategory
- {
- Weapons,
- Ammo,
- Explosives,
- Misc,
- Bundles,
- Tools,
- Utility,
- Job,
- Armor,
- Pointless,
- }
-}
diff --git a/Content.Shared/PDA/UplinkStoreListingPrototype.cs b/Content.Shared/PDA/UplinkStoreListingPrototype.cs
deleted file mode 100644
index f997380e7f..0000000000
--- a/Content.Shared/PDA/UplinkStoreListingPrototype.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Content.Shared.Roles;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.PDA
-{
- [Prototype("uplinkListing")]
- public sealed class UplinkStoreListingPrototype : IPrototype
- {
- [ViewVariables]
- [IdDataFieldAttribute]
- public string ID { get; } = default!;
-
- [DataField("itemId", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string ItemId { get; } = string.Empty;
-
- [DataField("price")]
- public int Price { get; } = 5;
-
- [DataField("category")]
- public UplinkCategory Category { get; } = UplinkCategory.Utility;
-
- [DataField("description")]
- public string Description { get; } = string.Empty;
-
- [DataField("listingName")]
- public string ListingName { get; } = string.Empty;
-
- [DataField("icon")]
- public SpriteSpecifier? Icon { get; } = null;
-
- [DataField("jobWhitelist", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))]
- public HashSet? JobWhitelist;
-
- [DataField("surplus")]
- public bool CanSurplus = true;
- }
-}
diff --git a/Content.Shared/Store/CurrencyPrototype.cs b/Content.Shared/Store/CurrencyPrototype.cs
new file mode 100644
index 0000000000..79438e5a75
--- /dev/null
+++ b/Content.Shared/Store/CurrencyPrototype.cs
@@ -0,0 +1,43 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Store;
+
+///
+/// Prototype used to define different types of currency for generic stores.
+/// Mainly used for antags, such as traitors, nukies, and revenants
+/// This is separate to the cargo ordering system.
+///
+[Prototype("currency")]
+[DataDefinition, Serializable, NetSerializable]
+public sealed class CurrencyPrototype : IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// The Loc string used for displaying the balance of a certain currency at the top of the store ui
+ ///
+ [DataField("balanceDisplay")]
+ public string BalanceDisplay { get; } = string.Empty;
+
+ ///
+ /// The Loc string used for displaying the price of listings in store UI
+ ///
+ [DataField("priceDisplay")]
+ public string PriceDisplay { get; } = string.Empty;
+
+ ///
+ /// The physical entity of the currency
+ ///
+ [DataField("entityId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? EntityId { get; }
+
+ ///
+ /// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId.
+ ///
+ [DataField("canWithdraw")]
+ public bool CanWithdraw { get; } = true;
+}
diff --git a/Content.Shared/Store/ListingCondition.cs b/Content.Shared/Store/ListingCondition.cs
new file mode 100644
index 0000000000..77d9bacbcf
--- /dev/null
+++ b/Content.Shared/Store/ListingCondition.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Store;
+
+///
+/// Used to define a complicated condition that requires C#
+///
+[ImplicitDataDefinitionForInheritors]
+[MeansImplicitUse]
+public abstract class ListingCondition
+{
+ ///
+ /// Determines whether or not a certain entity can purchase a listing.
+ ///
+ /// Whether or not the listing can be purchased
+ public abstract bool Condition(ListingConditionArgs args);
+}
+
+/// The person purchasing the listing
+/// The liting itself
+/// An entitymanager for sane coding
+public readonly record struct ListingConditionArgs(EntityUid Buyer, EntityUid? StoreEntity, ListingData Listing, IEntityManager EntityManager);
diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs
new file mode 100644
index 0000000000..5be7d00675
--- /dev/null
+++ b/Content.Shared/Store/ListingPrototype.cs
@@ -0,0 +1,133 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Utility;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.FixedPoint;
+using System.Linq;
+
+namespace Content.Shared.Store;
+
+///
+/// This is the data object for a store listing which is passed around in code.
+/// this allows for prices and features of listings to be dynamically changed in code
+/// without having to modify the prototypes.
+///
+[Serializable, NetSerializable]
+[Virtual, DataDefinition]
+public class ListingData : IEquatable
+{
+ ///
+ /// The name of the listing. If empty, uses the entity's name (if present)
+ ///
+ [DataField("name")]
+ public string Name = string.Empty;
+
+ ///
+ /// The description of the listing. If empty, uses the entity's description (if present)
+ ///
+ [DataField("description")]
+ public string Description = string.Empty;
+
+ ///
+ /// The categories that this listing applies to. Used for filtering a listing for a store.
+ ///
+ [DataField("categories", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List Categories = new();
+
+ ///
+ /// The cost of the listing. String represents the currency type while the FixedPoint2 represents the amount of that currency.
+ ///
+ [DataField("cost", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary Cost = new();
+
+ ///
+ /// Specific customizeable conditions that determine whether or not the listing can be purchased.
+ ///
+ [NonSerialized]
+ [DataField("conditions", serverOnly: true)]
+ public List? Conditions;
+
+ ///
+ /// The icon for the listing. If null, uses the icon for the entity or action.
+ ///
+ [DataField("icon")]
+ public SpriteSpecifier? Icon;
+
+ ///
+ /// The priority for what order the listings will show up in on the menu.
+ ///
+ [DataField("priority")]
+ public int Priority = 0;
+
+ ///
+ /// The entity that is given when the listing is purchased.
+ ///
+ [DataField("productEntity", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? ProductEntity;
+
+ ///
+ /// The action that is given when the listing is purchased.
+ ///
+ [DataField("productAction", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? ProductAction;
+
+ ///
+ /// The event that is broadcast when the listing is purchased.
+ ///
+ [DataField("productEvent")]
+ public object? ProductEvent;
+
+ ///
+ /// used internally for tracking how many times an item was purchased.
+ ///
+ public int PurchaseAmount = 0;
+
+ public bool Equals(ListingData? listing)
+ {
+ if (listing == null)
+ return false;
+
+ //simple conditions
+ if (Priority != listing.Priority ||
+ Name != listing.Name ||
+ Description != listing.Description ||
+ ProductEntity != listing.ProductEntity ||
+ ProductAction != listing.ProductAction ||
+ ProductEvent != listing.ProductEvent)
+ return false;
+
+ if (Icon != null && !Icon.Equals(listing.Icon))
+ return false;
+
+ ///more complicated conditions that eat perf. these don't really matter
+ ///as much because you will very rarely have to check these.
+ if (!Categories.OrderBy(x => x).SequenceEqual(listing.Categories.OrderBy(x => x)))
+ return false;
+
+ if (!Cost.OrderBy(x => x).SequenceEqual(listing.Cost.OrderBy(x => x)))
+ return false;
+
+ if ((Conditions != null && listing.Conditions != null) &&
+ !Conditions.OrderBy(x => x).SequenceEqual(listing.Conditions.OrderBy(x => x)))
+ return false;
+
+ return true;
+ }
+}
+
+//
+///
+/// Defines a set item listing that is available in a store
+///
+[Prototype("listing")]
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed class ListingPrototype : ListingData, IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; } = default!;
+}
diff --git a/Content.Shared/Store/StoreCategoryPrototype.cs b/Content.Shared/Store/StoreCategoryPrototype.cs
new file mode 100644
index 0000000000..739184217b
--- /dev/null
+++ b/Content.Shared/Store/StoreCategoryPrototype.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Store;
+
+///
+/// Used to define different categories for a store.
+///
+[Prototype("storeCategory")]
+[Serializable, NetSerializable, DataDefinition]
+public sealed class StoreCategoryPrototype : IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ [DataField("name")]
+ public string Name { get; } = string.Empty;
+
+ [DataField("priority")]
+ public int Priority { get; } = 0;
+}
diff --git a/Content.Shared/Store/StorePresetPrototype.cs b/Content.Shared/Store/StorePresetPrototype.cs
new file mode 100644
index 0000000000..961f8cbf95
--- /dev/null
+++ b/Content.Shared/Store/StorePresetPrototype.cs
@@ -0,0 +1,41 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Content.Shared.FixedPoint;
+
+namespace Content.Shared.Store;
+
+///
+/// Specifies generic info for initializing a store.
+///
+[Prototype("storePreset")]
+[DataDefinition]
+public sealed class StorePresetPrototype : IPrototype
+{
+ [ViewVariables] [IdDataField] public string ID { get; } = default!;
+
+ ///
+ /// The name displayed at the top of the store window
+ ///
+ [DataField("storeName", required: true)]
+ public string StoreName { get; } = string.Empty;
+
+ ///
+ /// The categories that this store can access
+ ///
+ [DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet Categories { get; } = new();
+
+ ///
+ /// The inital balance that the store initializes with.
+ ///
+ [DataField("initialBalance",
+ customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary? InitialBalance { get; }
+
+ ///
+ /// The currencies that are accepted in the store
+ ///
+ [DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ public HashSet CurrencyWhitelist { get; } = new();
+}
diff --git a/Content.Shared/Store/StoreUi.cs b/Content.Shared/Store/StoreUi.cs
new file mode 100644
index 0000000000..f364c11377
--- /dev/null
+++ b/Content.Shared/Store/StoreUi.cs
@@ -0,0 +1,84 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.MobState;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Store;
+
+[Serializable, NetSerializable]
+public enum StoreUiKey : byte
+{
+ Key
+}
+
+[Serializable, NetSerializable]
+public sealed class StoreUpdateState : BoundUserInterfaceState
+{
+ public readonly EntityUid? Buyer;
+
+ public readonly HashSet Listings;
+
+ public readonly Dictionary Balance;
+
+ public StoreUpdateState(EntityUid? buyer, HashSet listings, Dictionary balance)
+ {
+ Buyer = buyer;
+ Listings = listings;
+ Balance = balance;
+ }
+}
+
+///
+/// initializes miscellaneous data about the store.
+///
+[Serializable, NetSerializable]
+public sealed class StoreInitializeState : BoundUserInterfaceState
+{
+ public readonly string Name;
+
+ public StoreInitializeState(string name)
+ {
+ Name = name;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
+{
+ public EntityUid CurrentBuyer;
+
+ public StoreRequestUpdateInterfaceMessage(EntityUid currentBuyer)
+ {
+ CurrentBuyer = currentBuyer;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class StoreBuyListingMessage : BoundUserInterfaceMessage
+{
+ public EntityUid Buyer;
+
+ public ListingData Listing;
+
+ public StoreBuyListingMessage(EntityUid buyer, ListingData listing)
+ {
+ Buyer = buyer;
+ Listing = listing;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class StoreRequestWithdrawMessage : BoundUserInterfaceMessage
+{
+ public EntityUid Buyer;
+
+ public string Currency;
+
+ public int Amount;
+
+ public StoreRequestWithdrawMessage(EntityUid buyer, string currency, int amount)
+ {
+ Buyer = buyer;
+ Currency = currency;
+ Amount = amount;
+ }
+}
diff --git a/Content.Shared/Traitor/Uplink/UplinkAccount.cs b/Content.Shared/Traitor/Uplink/UplinkAccount.cs
deleted file mode 100644
index f27399a2ae..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkAccount.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Shared.Traitor.Uplink
-{
- public sealed class UplinkAccount
- {
- public readonly EntityUid? AccountHolder;
- public int Balance;
-
- public UplinkAccount(int startingBalance, EntityUid? accountHolder = null)
- {
- AccountHolder = accountHolder;
- Balance = startingBalance;
- }
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkAccountData.cs b/Content.Shared/Traitor/Uplink/UplinkAccountData.cs
deleted file mode 100644
index 2d4a392a30..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkAccountData.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Content.Shared.Roles;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public sealed class UplinkAccountData
- {
- public EntityUid? DataAccountHolder;
- public int DataBalance;
- public UplinkAccountData(EntityUid? dataAccountHolder, int dataBalance)
- {
- DataAccountHolder = dataAccountHolder;
- DataBalance = dataBalance;
- }
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkListingData.cs b/Content.Shared/Traitor/Uplink/UplinkListingData.cs
deleted file mode 100644
index a6e5ac65ed..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkListingData.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Content.Shared.PDA;
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public sealed class UplinkListingData : ComponentState, IEquatable
- {
- public readonly string ItemId;
- public readonly int Price;
- public readonly UplinkCategory Category;
- public readonly string Description;
- public readonly string ListingName;
- public readonly SpriteSpecifier? Icon;
- public readonly HashSet? JobWhitelist;
-
- public UplinkListingData(string listingName, string itemId,
- int price, UplinkCategory category,
- string description, SpriteSpecifier? icon, HashSet? jobWhitelist)
- {
- ListingName = listingName;
- Price = price;
- Category = category;
- Description = description;
- ItemId = itemId;
- Icon = icon;
- JobWhitelist = jobWhitelist;
- }
-
- public bool Equals(UplinkListingData? other)
- {
- if (other == null)
- {
- return false;
- }
-
- return ItemId == other.ItemId;
- }
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkMessagesUI.cs b/Content.Shared/Traitor/Uplink/UplinkMessagesUI.cs
deleted file mode 100644
index d783476285..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkMessagesUI.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public sealed class UplinkBuyListingMessage : BoundUserInterfaceMessage
- {
- public string ItemId;
-
- public UplinkBuyListingMessage(string itemId)
- {
- ItemId = itemId;
- }
- }
-
- [Serializable, NetSerializable]
- public sealed class UplinkRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
- {
- public UplinkRequestUpdateInterfaceMessage()
- {
-
- }
- }
-
- [Serializable, NetSerializable]
- public sealed class UplinkTryWithdrawTC : BoundUserInterfaceMessage
- {
- public int TC;
-
- public UplinkTryWithdrawTC(int tc)
- {
- TC = tc;
- }
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkNetworkEvents.cs b/Content.Shared/Traitor/Uplink/UplinkNetworkEvents.cs
deleted file mode 100644
index b524f84df3..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkNetworkEvents.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public sealed class UplinkBuySuccessMessage : EntityEventArgs
- {
- }
-
- [Serializable, NetSerializable]
- public sealed class UplinkInsufficientFundsMessage : EntityEventArgs
- {
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkUpdateState.cs b/Content.Shared/Traitor/Uplink/UplinkUpdateState.cs
deleted file mode 100644
index 6d13316ffe..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkUpdateState.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public sealed class UplinkUpdateState : BoundUserInterfaceState
- {
- public UplinkAccountData Account;
- public UplinkListingData[] Listings;
-
- public UplinkUpdateState(UplinkAccountData account, UplinkListingData[] listings)
- {
- Account = account;
- Listings = listings;
- }
- }
-}
diff --git a/Content.Shared/Traitor/Uplink/UplinkVisuals.cs b/Content.Shared/Traitor/Uplink/UplinkVisuals.cs
deleted file mode 100644
index e171100608..0000000000
--- a/Content.Shared/Traitor/Uplink/UplinkVisuals.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Traitor.Uplink
-{
- [Serializable, NetSerializable]
- public enum UplinkUiKey : byte
- {
- Key
- }
-}
diff --git a/Resources/Locale/en-US/store/currency.ftl b/Resources/Locale/en-US/store/currency.ftl
new file mode 100644
index 0000000000..83853d4e5c
--- /dev/null
+++ b/Resources/Locale/en-US/store/currency.ftl
@@ -0,0 +1,11 @@
+store-currency-inserted = {CAPITALIZE(THE($used))} is inserted into the {THE($target)}.
+
+store-currency-free = Free
+store-currency-balance-display-debugdollar = Debug Dollar: {$amount}
+store-currency-price-display-debugdollar = {$amount ->
+ [one] {$amount} Debug Dollar
+ *[other] {$amount} Debug Dollars
+}
+
+store-currency-balance-display-telecrystal = TC: {$amount}
+store-currency-price-display-telecrystal = {$amount} TC
\ No newline at end of file
diff --git a/Resources/Locale/en-US/store/store.ftl b/Resources/Locale/en-US/store/store.ftl
new file mode 100644
index 0000000000..b7dc76cb08
--- /dev/null
+++ b/Resources/Locale/en-US/store/store.ftl
@@ -0,0 +1,4 @@
+store-ui-default-title = Store
+store-ui-default-withdraw-text = Withdraw
+
+store-withdraw-button-ui = Withdraw {$currency}
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/catalog.yml b/Resources/Prototypes/Catalog/catalog.yml
new file mode 100644
index 0000000000..9b09de8a57
--- /dev/null
+++ b/Resources/Prototypes/Catalog/catalog.yml
@@ -0,0 +1,44 @@
+- type: listing
+ id: DebugListing
+ name: debug name
+ description: debug desc
+ categories:
+ - Debug
+ cost:
+ DebugDollar: 10
+ Telecrystal: 10
+
+- type: listing
+ id: DebugListing3
+ name: debug name 3
+ description: debug desc 3
+ categories:
+ - Debug
+ cost:
+ DebugDollar: 10
+
+- type: listing
+ id: DebugListing5
+ name: debug name 5
+ description: debug desc 5
+ categories:
+ - Debug
+
+- type: listing
+ id: DebugListing4
+ name: debug name 4
+ description: debug desc 4
+ productAction: Scream
+ categories:
+ - Debug
+ cost:
+ DebugDollar: 1
+
+- type: listing
+ id: DebugListing2
+ name: debug name 2
+ description: debug desc 2
+ categories:
+ - Debug2
+ cost:
+ DebugDollar: 10
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index 1111957588..8f12524f0d 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -1,500 +1,641 @@
# Guns
-- type: uplinkListing
+- type: listing
id: UplinkPistolViper
- category: Weapons
- itemId: WeaponPistolViper
- listingName: Viper
+ name: Viper
description: A small, easily concealable, but somewhat underpowered gun. Use pistol magazines (.35 auto).
- price: 6
+ productEntity: WeaponPistolViper
+ cost:
+ Telecrystal: 6
+ categories:
+ - UplinkWeapons
-- type: uplinkListing
+- type: listing
id: UplinkRevolverPython
- category: Weapons
- itemId: WeaponRevolverPython
- listingName: Python
+ name: Python
description: A loud and deadly revolver. Uses .40 Magnum.
- price: 8
+ productEntity: WeaponRevolverPython
+ cost:
+ Telecrystal: 8
+ categories:
+ - UplinkWeapons
# Inbuilt suppressor so it's sneaky + more expensive.
-- type: uplinkListing
+- type: listing
id: UplinkPistolCobra
- category: Weapons
- itemId: WeaponPistolCobra
- listingName: Cobra
+ name: Cobra
description: A rugged, robust operator handgun with inbuilt silencer. Use pistol magazines (.25 caseless).
- price: 8
+ productEntity: WeaponPistolCobra
+ cost:
+ Telecrystal: 8
+ categories:
+ - UplinkWeapons
# Poor accuracy, slow to fire, cheap option
-- type: uplinkListing
+- type: listing
id: UplinkRifleMosin
- category: Weapons
- itemId: WeaponSniperMosin
- listingName: Surplus Rifle
+ name: Surplus Rifle
description: A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap.
- price: 4
+ productEntity: WeaponSniperMosin
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkWeapons
-#- type: uplinkListing
-# id: UplinkCrossbowEnergyMini
-# category: Weapons
-# itemId: CrossbowEnergyMini
-# price: 8
-
-
-- type: uplinkListing
+- type: listing
id: UplinkEsword
- category: Weapons
- itemId: EnergySword
- listingName: Energy Sword
+ name: Energy Sword
description: A very dangerous energy sword. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
- price: 8
+ productEntity: EnergySword
+ cost:
+ Telecrystal: 8
+ categories:
+ - UplinkWeapons
-# bug swept to make
-#- type: uplinkListing
-# id: UplinkDoubleBladedESword
-# category: Weapons
-# itemId: DoubleBladedESword
-# price: 16
-
-- type: uplinkListing
+- type: listing
id: UplinkEnergyDagger
- category: Weapons
- itemId: EnergyDagger
- listingName: Energy Dagger
+ name: Energy Dagger
description: A small energy blade conveniently disguised in the form of a pen.
- price: 2
+ productEntity: EnergyDagger
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkWeapons
-- type: uplinkListing
+- type: listing
id: UplinkFireAxeFlaming
- category: Weapons
- itemId: FireAxeFlaming
- listingName: Fire Axe
+ name: Fire Axe
description: A classic-style weapon infused with advanced atmos technology to allow it to set targets on fire.
- price: 10
+ productEntity: FireAxeFlaming
+ cost:
+ Telecrystal: 10
+ categories:
+ - UplinkWeapons
# Explosives
-- type: uplinkListing
+- type: listing
id: UplinkExplosiveGrenade
- category: Explosives
- itemId: ExGrenade
- price: 4
+ productEntity: ExGrenade
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkExplosives
-- type: uplinkListing
+- type: listing
id: UplinkExplosiveGrenadeFlash
- category: Explosives
- itemId: GrenadeFlashBang
- price: 2
+ productEntity: GrenadeFlashBang
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkExplosives
-- type: uplinkListing
+- type: listing
id: UplinkSyndieMiniBomb
- category: Explosives
- itemId: SyndieMiniBomb
- price: 7
+ productEntity: SyndieMiniBomb
+ cost:
+ Telecrystal: 7
+ categories:
+ - UplinkExplosives
-- type: uplinkListing
+- type: listing
id: UplinkGrenadePenguin
- category: Explosives
- itemId: MobGrenadePenguin
- price: 6
- surplus: false # got wrecked by penguins from surplus crate
+ productEntity: MobGrenadePenguin
+ cost:
+ Telecrystal: 6
+ categories:
+ - UplinkExplosives
+ conditions:
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
-- type: uplinkListing
+- type: listing
id: UplinkC4
- category: Explosives
- itemId: C4
- price: 2
description: >
C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, airlocks or sabotage equipment.
It can be attached to almost all objects and has a modifiable timer with a minimum setting of 10 seconds.
+ productEntity: C4
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkExplosives
-- type: uplinkListing
+- type: listing
id: UplinkC4Bundle
- category: Explosives
- itemId: ClothingBackpackDuffelSyndicateC4tBundle
- price: 12 # 25% off
description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives.
+ productEntity: ClothingBackpackDuffelSyndicateC4tBundle
+ cost:
+ Telecrystal: 12
+ categories:
+ - UplinkExplosives
# Ammo
-- type: uplinkListing
+- type: listing
id: UplinkPistol9mmMagazine
- category: Ammo
- itemId: MagazinePistol
- price: 2
+ productEntity: MagazinePistol
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkAmmo
# For the Mandella
-- type: uplinkListing
+- type: listing
id: UplinkMagazinePistolCaselessRifle
- category: Ammo
- itemId: MagazinePistolCaselessRifle
- price: 2
+ productEntity: MagazinePistolCaselessRifle
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkAmmo
# For the Inspector
-- type: uplinkListing
+- type: listing
id: UplinkSpeedLoaderMagnum
- category: Ammo
- itemId: SpeedLoaderMagnum
- price: 2
icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_speed_loader.rsi/base.png
+ productEntity: SpeedLoaderMagnum
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkAmmo
# For the mosin
-- type: uplinkListing
+- type: listing
id: UplinkMosinAmmo
- category: Ammo
- itemId: BoxMagazineLightRifle
description: A box of cartridges for the surplus rifle.
- price: 2
-
+ productEntity: BoxMagazineLightRifle
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkAmmo
#Utility
-- type: uplinkListing
+- type: listing
id: UplinkHoloparaKit
- category: Utility
- itemId: BoxHoloparasite
- listingName: Holoparasite Kit
+ name: Holoparasite Kit
description: The pride and joy of Cybersun. Contains an injector that hosts a sentient metaphysical guardian made of hard light which resides in the user's body when not active. The guardian can punch rapidly and is immune to hazardous environments and bullets, but shares any damage it takes with the user.
icon: /Textures/Objects/Misc/guardian_info.rsi/icon.png
- price: 14
+ productEntity: BoxHoloparasite
+ cost:
+ Telecrystal: 14
+ categories:
+ - UplinkUtility
+ conditions:
+ - !type:StoreWhitelistCondition
+ blacklist:
+ tags:
+ - NukeOpsUplink
-- type: uplinkListing
+- type: listing
id: UplinkHolster
- category: Utility
- itemId: ClothingBeltSyndieHolster
- listingName: Syndicate Shoulder Holster
+ name: Syndicate Shoulder Holster
description: A deep shoulder holster capable of holding many types of ballistics.
- icon: /Textures/Clothing/Belt/syndieholster.rsi/icon.png
- price: 2
+ productEntity: ClothingBeltSyndieHolster
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkUtility
-- type: uplinkListing
+- type: listing
id: UplinkEmag
- category: Utility
- itemId: Emag
+ name: Emag
description: The business card of the syndicate, this sequencer is able to break open airlocks and tamper with a variety of station devices. Recharges automatically.
- icon: /Textures/Objects/Tools/emag.rsi/icon.png
- price: 8
+ productEntity: Emag
+ cost:
+ Telecrystal: 8
+ categories:
+ - UplinkUtility
-- type: uplinkListing
+- type: listing
id: UplinkAgentIDCard
- category: Utility
- itemId: AgentIDCard
- listingName: Agent ID Card
+ name: Agent ID Card
description: A modified ID card that can copy accesses from other cards and change its name and job title at-will.
- icon: Objects/Misc/id_cards.rsi/default.png
- price: 3
+ productEntity: AgentIDCard
+ cost:
+ Telecrystal: 3
+ categories:
+ - UplinkUtility
-- type: uplinkListing
+- type: listing
id: UplinkJetpack
- category: Utility
- itemId: JetpackBlack
- listingName: Black Jetpack
+ name: Black Jetpack
description: A black jetpack. It allows you to fly around in space. Additional fuel not included.
- icon: Objects/Tanks/Jetpacks/black.rsi/icon.png
- price: 5
+ productEntity: JetpackBlack
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkUtility
-- type: uplinkListing
- id: ReinforcementTeleporterSyndicate
- category: Utility
- itemId: ReinforcementTeleporterSyndicate
- listingName: Reinforcement Teleporter
+- type: listing
+ id: UplinkReinforcementTeleporterSyndicate
+ name: Reinforcement Teleporter
description: Teleport in an agent of extremely questionable quality. No off button, buy this if you're ready to party. They have a pistol with no reserve ammo, and a knife. That's it.
+ productEntity: ReinforcementTeleporterSyndicate
icon: Objects/Devices/communication.rsi/old-radio.png
- price: 25
+ cost:
+ Telecrystal: 25
+ categories:
+ - UplinkUtility
#TODO: Increase the price of this to 4-5/remove it when we get encrpytion keys
-- type: uplinkListing
+- type: listing
id: UplinkHeadset
- category: Utility
- itemId: ClothingHeadsetAltSyndicate
- listingName: Syndicate Overear-Headset
+ name: Syndicate Overear-Headset
description: A headset that allows you to listen in on departmental channels, or contact other traitors.
- icon: Clothing/Ears/Headsets/syndicate.rsi/icon_alt.png
- price: 2
+ productEntity: ClothingHeadsetAltSyndicate
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkUtility
-- type: uplinkListing
+- type: listing
id: UplinkHypopen
- category: Utility
- itemId: Hypopen
- listingName: Hypopen
+ name: Hypopen
description: A chemical hypospray disguised as a pen, capable of instantly injecting up to 15u of reagents. Starts empty.
- price: 4
+ productEntity: Hypopen
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkUtility
# Bundles
-- type: uplinkListing
+- type: listing
id: UplinkC20RBundle
- category: Bundles
- itemId: ClothingBackpackDuffelSyndicateFilledSMG
- price: 25
icon: /Textures/Objects/Weapons/Guns/SMGs/c20r.rsi/icon.png
+ productEntity: ClothingBackpackDuffelSyndicateFilledSMG
+ cost:
+ Telecrystal: 25
+ categories:
+ - UplinkBundles
-- type: uplinkListing
+- type: listing
id: UplinkBulldogBundle
- category: Bundles
- itemId: ClothingBackpackDuffelSyndicateFilledShotgun
- price: 25
icon: /Textures/Objects/Weapons/Guns/Shotguns/bulldog.rsi/icon.png
+ productEntity: ClothingBackpackDuffelSyndicateFilledShotgun
+ cost:
+ Telecrystal: 25
+ categories:
+ - UplinkBundles
-- type: uplinkListing
+- type: listing
id: UplinkGrenadeLauncherBundle
- category: Bundles
- itemId: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
- price: 30
icon: /Textures/Objects/Weapons/Guns/Launchers/china_lake.rsi/icon.png
+ productEntity: ClothingBackpackDuffelSyndicateFilledGrenadeLauncher
+ cost:
+ Telecrystal: 30
+ categories:
+ - UplinkBundles
-- type: uplinkListing
+- type: listing
id: UplinkL6SawBundle
- category: Bundles
- itemId: ClothingBackpackDuffelSyndicateFilledLMG
- price: 40
icon: /Textures/Objects/Weapons/Guns/LMGs/l6.rsi/icon.png
+ productEntity: ClothingBackpackDuffelSyndicateFilledLMG
+ cost:
+ Telecrystal: 40
+ categories:
+ - UplinkBundles
-# Add this back in once war ops/separate nukie inventories are added.
-#- type: uplinkListing
-# id: UplinkZombieBundle
-# category: Bundles
-# itemId: ClothingBackpackDuffelZombieBundle
-# price: 50
-# icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
+- type: listing
+ id: UplinkZombieBundle
+ icon: /Textures/Structures/Wallmounts/signs.rsi/bio.png
+ productEntity: ClothingBackpackDuffelZombieBundle
+ cost:
+ Telecrystal: 40
+ categories:
+ - UplinkBundles
+ conditions:
+ - !type:StoreWhitelistCondition
+ whitelist:
+ tags:
+ - NukeOpsUplink
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
-- type: uplinkListing
+- type: listing
id: UplinkSurplusBundle
- category: Bundles
- itemId: CrateSyndicateSurplusBundle
description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good.
- price: 20
- surplus: false
+ productEntity: CrateSyndicateSurplusBundle
+ cost:
+ Telecrystal: 20
+ categories:
+ - UplinkBundles
+ conditions:
+ - !type:StoreWhitelistCondition
+ blacklist:
+ tags:
+ - NukeOpsUplink
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
-- type: uplinkListing
+- type: listing
id: UplinkSuperSurplusBundle
- category: Bundles
- itemId: CrateSyndicateSuperSurplusBundle
description: Contains 125 telecrystals worth of completely random Syndicate items.
- price: 40
- surplus: false
-
-#- type: uplinkListing
-# id: UplinkCarbineBundle
-# category: Bundles
-# itemId: ClothingBackpackDuffelSyndicateFilledCarbine
-# price: 35
-# icon: /Textures/Objects/Weapons/Guns/Rifles/carbine.rsi/icon.png
+ productEntity: CrateSyndicateSuperSurplusBundle
+ cost:
+ Telecrystal: 40
+ categories:
+ - UplinkBundles
+ conditions:
+ - !type:StoreWhitelistCondition
+ blacklist:
+ tags:
+ - NukeOpsUplink
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
# Tools
-- type: uplinkListing
+- type: listing
id: UplinkToolbox
- category: Tools
- itemId: ToolboxSyndicateFilled
- price: 2
+ productEntity: ToolboxSyndicateFilled
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkTools
-- type: uplinkListing
+- type: listing
id: UplinkSyndicateJawsOfLife
- category: Tools
- itemId: SyndicateJawsOfLife
- price: 2
+ productEntity: SyndicateJawsOfLife
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkTools
-- type: uplinkListing
+- type: listing
id: UplinkDuffelSurgery
- category: Tools
- itemId: ClothingBackpackDuffelSyndicateFilledMedical
- price: 5
+ productEntity: ClothingBackpackDuffelSyndicateFilledMedical
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkTools
-- type : uplinkListing
+- type: listing
id: UplinkPowerSink
- category: Tools
- itemId: PowerSink
- price: 5
+ productEntity: PowerSink
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkTools
-- type: uplinkListing
+- type: listing
id: UplinkCarpDehydrated
- category: Tools
- itemId: DehydratedSpaceCarp
- price: 3
+ productEntity: DehydratedSpaceCarp
+ cost:
+ Telecrystal: 3
+ categories:
+ - UplinkTools
# Job Specific
-- type: uplinkListing
+- type: listing
id: uplinkGatfruitSeeds
- category: Job
- itemId: GatfruitSeeds
description: And who says guns don't grow on trees?
- price: 4
- jobWhitelist:
- - Botanist
+ productEntity: GatfruitSeeds
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkJob
+ conditions:
+ - !type:BuyerJobCondition
+ whitelist:
+ - Botanist
-- type: uplinkListing
+- type: listing
id: uplinkNecronomicon
- category: Job
- itemId: BibleNecronomicon
description: An unholy book capable of summoning a demonic familiar.
- price: 6
- surplus: false
- jobWhitelist:
- - Chaplain
+ productEntity: BibleNecronomicon
+ cost:
+ Telecrystal: 6
+ categories:
+ - UplinkJob
+ conditions:
+ - !type:BuyerJobCondition
+ whitelist:
+ - Chaplain
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
# Armor
# Should be cameleon shoes, change when implemented.
-- type: uplinkListing
+- type: listing
id: UplinkClothingNoSlipsShoes
- category: Armor
- itemId: ClothingShoesChameleonNoSlips
- listingName: no-slip shoes
+ name: no-slip shoes
description: These protect you from slips while looking like normal sneakers.
- price: 2
+ productEntity: ClothingShoesChameleonNoSlips
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkgClothingThievingGloves
- category: Armor
- itemId: ThievingGloves
- listingName: Thieving Gloves
+ name: Thieving Gloves
description: Discretely steal from pockets and increase your thieving technique with these fancy new gloves, all while looking like normal gloves!
- price: 4
+ productEntity: ThievingGloves
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkClothingOuterVestWeb
- category: Armor
- itemId: ClothingOuterVestWeb
- price: 5
+ productEntity: ClothingOuterVestWeb
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkHardsuitSyndie
- category: Armor
- itemId: ClothingBackpackDuffelSyndicateHardsuitBundle
description: The Syndicate's well known armored blood red hardsuit, capable of space walks and bullet resistant.
- price: 8
+ productEntity: ClothingBackpackDuffelSyndicateHardsuitBundle
+ cost:
+ Telecrystal: 8
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkClothingShoesBootsMagSyndie
- category: Armor
- itemId: ClothingShoesBootsMagSyndie
description: A pair of magnetic boots that will keep you on the ground if the gravity fails or is sabotaged, giving you a mobility advantage. If activated with gravity they will protect from slips, but they will slow you down.
- price: 2
+ productEntity: ClothingShoesBootsMagSyndie
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkEVASyndie
- category: Armor
- itemId: ClothingBackpackDuffelSyndicateEVABundle
description: A simple EVA suit that offers no protection other than what's needed to survive in space.
- price: 4
+ productEntity: ClothingBackpackDuffelSyndicateEVABundle
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkArmor
-- type: uplinkListing
+- type: listing
id: UplinkClothingOuterHardsuitJuggernaut
- category: Armor
- itemId: ClothingOuterHardsuitJuggernaut
description: Hyper resilient armor made of materials tested in the Tau chromosphere facility. The only thing that's going to be slowing you down is this suit... and tasers.
- price: 12
-
+ productEntity: ClothingOuterHardsuitJuggernaut
+ cost:
+ Telecrystal: 12
+ categories:
+ - UplinkArmor
# Misc
-- type: uplinkListing
+- type: listing
id: UplinkCyberpen
- category: Misc
- itemId: CyberPen
description: Cybersun's legal department pen. Smells vaguely of hard-light and war profiteering.
- price: 4
+ productEntity: CyberPen
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkDecoyDisk
- category: Misc
- itemId: NukeDiskFake
- listingName: Decoy nuclear disk
+ name: decoy nuclear disk
description: A piece of plastic with a lenticular printing, made to look like a nuclear auth disk.
- price: 1
+ productEntity: NukeDiskFake
+ cost:
+ Telecrystal: 1
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkRevolverCapGun
- category: Misc
- itemId: RevolverCapGun
- price: 4
+ productEntity: RevolverCapGun
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkCigarettes
- category: Misc
- itemId: CigPackSyndicate
- price: 2
+ productEntity: CigPackSyndicate
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkSoapSyndie
- category: Misc
- itemId: SoapSyndie
- price: 1
+ productEntity: SoapSyndie
+ cost:
+ Telecrystal: 1
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkUltrabrightLantern
- category: Misc
- itemId: lanternextrabright
- price: 2
+ productEntity: lanternextrabright #why is this item id not capitalized???
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkMisc
-#- type: uplinkListing
+#- type: listing
# id: UplinkCostumeCentcom
-# category: Misc
-# itemId: ClothingBackpackDuffelSyndicateCostumeCentcom
-# price: 4
+# productEntity: ClothingBackpackDuffelSyndicateCostumeCentcom
+# cost:
+# Telecrystal: 4
+# categories:
+# - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkGigacancerScanner
- category: Misc
- itemId: HandheldHealthAnalyzerGigacancer
- listingName: Ultragigacancer Health Analyzer
+ name: Ultragigacancer Health Analyzer
description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer.
- price: 5
+ productEntity: HandheldHealthAnalyzerGigacancer
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkNocturineChemistryBottle
- category: Misc
- itemId: NocturineChemistryBottle
description: A chemical that makes it very hard for your target to stand up.
- price: 5
+ productEntity: NocturineChemistryBottle
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkMisc
-- type: uplinkListing
+- type: listing
id: UplinkSyndicateSegwayCrate
- category: Misc
- itemId: CrateFunSyndicateSegway
- listingName: syndicate segway
+ name: syndicate segway
description: Be an enemy of the corporation, in style!
- price: 5
- surplus: false
+ productEntity: CrateFunSyndicateSegway
+ cost:
+ Telecrystal: 5
+ categories:
+ - UplinkMisc
+ conditions:
+ - !type:BuyerWhitelistCondition
+ blacklist:
+ components:
+ - SurplusBundle
# Pointless
-- type: uplinkListing
+- type: listing
id: UplinkSyndicateStamp
- category: Pointless
- itemId: RubberStampSyndicate
- price: 2
+ productEntity: RubberStampSyndicate
+ cost:
+ Telecrystal: 2
+ categories:
+ - UplinkPointless
-- type: uplinkListing
+- type: listing
id: UplinkCatEars
- category: Pointless
- itemId: ClothingHeadHatCatEars
- listingName: Cat Ears
- description: UwU.
- price: 21
+ name: Cat Ears
+ description: UwU
+ productEntity: ClothingHeadHatCatEars
+ cost:
+ Telecrystal: 21
+ categories:
+ - UplinkPointless
-- type: uplinkListing
+- type: listing
id: UplinkOutlawHat
- category: Pointless
- itemId: ClothingHeadHatOutlawHat
- price: 1
+ productEntity: ClothingHeadHatOutlawHat
+ cost:
+ Telecrystal: 1
+ categories:
+ - UplinkPointless
-- type: uplinkListing
+- type: listing
id: UplinkCostumePyjama
- category: Pointless
- itemId: ClothingBackpackDuffelSyndicatePyjamaBundle
- price: 4
+ productEntity: ClothingBackpackDuffelSyndicatePyjamaBundle
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkPointless
-- type: uplinkListing
+- type: listing
id: UplinkCostumeClown
- category: Pointless
- itemId: ClothingBackpackDuffelSyndicateCostumeClown
- price: 4
+ productEntity: ClothingBackpackDuffelSyndicateCostumeClown
+ cost:
+ Telecrystal: 4
+ categories:
+ - UplinkPointless
-- type: uplinkListing
+- type: listing
id: UplinkBalloon
- category: Pointless
- itemId: BalloonSyn
- price: 20
+ productEntity: BalloonSyn
+ cost:
+ Telecrystal: 20
+ categories:
+ - UplinkPointless
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 375f57b865..b671b97ef0 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -48,8 +48,8 @@
interfaces:
- key: enum.PDAUiKey.Key
type: PDABoundUserInterface
- - key: enum.UplinkUiKey.Key
- type: UplinkBoundUserInterface
+ - key: enum.StoreUiKey.Key
+ type: StoreBoundUserInterface
- key: enum.RingerUiKey.Key
type: RingerBoundUserInterface
- key: enum.InstrumentUiKey.Key
diff --git a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml
index 7c99b4ebd1..762a2e051f 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml
@@ -15,9 +15,11 @@
count: 20
max: 999999 # todo: add support for unlimited stacks
stackType: Telecrystal
- - type: Telecrystal
- type: StackPrice
price: 200
+ - type: Currency
+ price:
+ Telecrystal: 1
- type: entity
parent: Telecrystal
@@ -61,45 +63,55 @@
heldPrefix: old-radio
- type: UserInterface
interfaces:
- - key: enum.UplinkUiKey.Key
- type: UplinkBoundUserInterface
- - type: Uplink
- activatesInHands: true
-
+ - key: enum.StoreUiKey.Key
+ type: StoreBoundUserInterface
+ - type: ActivatableUI
+ key: enum.StoreUiKey.Key
+ - type: Store
+ preset: StorePresetUplink
+ balance:
+ Telecrystal: 0
+
- type: entity
parent: BaseUplinkRadio
id: BaseUplinkRadio20TC
suffix: 20 TC
components:
- - type: Uplink
- presetInfo:
- balance: 20
+ - type: Store
+ preset: StorePresetUplink
+ balance:
+ Telecrystal: 20
-
-#Default Nuclear Operative amount, not considering crew count
- type: entity
parent: BaseUplinkRadio
id: BaseUplinkRadio25TC
suffix: 25 TC
components:
- - type: Uplink
- presetInfo:
- balance: 25
+ - type: Store
+ preset: StorePresetUplink
+ balance:
+ Telecrystal: 25
+#this uplink MUST be used for nukeops, as it has the tag for filtering the listing.
- type: entity
parent: BaseUplinkRadio
id: BaseUplinkRadio40TC
- suffix: 40 TC
+ suffix: 40 TC, NukeOps
components:
- - type: Uplink
- presetInfo:
- balance: 40
+ - type: Store
+ preset: StorePresetUplink
+ balance:
+ Telecrystal: 40
+ - type: Tag
+ tags:
+ - NukeOpsUplink
- type: entity
parent: BaseUplinkRadio
id: BaseUplinkRadioDebug
suffix: Debug
components:
- - type: Uplink
- presetInfo:
- balance: 9999
+ - type: Store
+ preset: StorePresetUplink
+ balance:
+ Telecrystal: 99999
diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml
new file mode 100644
index 0000000000..135500ccce
--- /dev/null
+++ b/Resources/Prototypes/Store/categories.yml
@@ -0,0 +1,60 @@
+#debug
+- type: storeCategory
+ id: Debug
+ name: debug category
+
+- type: storeCategory
+ id: Debug2
+ name: debug category 2
+
+#uplink categoires
+- type: storeCategory
+ id: UplinkWeapons
+ name: Weapons
+ priority: 0
+
+- type: storeCategory
+ id: UplinkAmmo
+ name: Ammo
+ priority: 1
+
+- type: storeCategory
+ id: UplinkExplosives
+ name: Explosives
+ priority: 2
+
+- type: storeCategory
+ id: UplinkMisc
+ name: Misc
+ priority: 3
+
+- type: storeCategory
+ id: UplinkBundles
+ name: Bundles
+ priority: 4
+
+- type: storeCategory
+ id: UplinkTools
+ name: Tools
+ priority: 5
+
+- type: storeCategory
+ id: UplinkUtility
+ name: Utility
+ priority: 6
+
+- type: storeCategory
+ id: UplinkJob
+ name: Job
+ priority: 7
+
+- type: storeCategory
+ id: UplinkArmor
+ name: Armor
+ priority: 8
+
+- type: storeCategory
+ id: UplinkPointless
+ name: Pointless
+ priority: 9
+
diff --git a/Resources/Prototypes/Store/currency.yml b/Resources/Prototypes/Store/currency.yml
new file mode 100644
index 0000000000..1ff3b94f99
--- /dev/null
+++ b/Resources/Prototypes/Store/currency.yml
@@ -0,0 +1,12 @@
+- type: currency
+ id: Telecrystal
+ balanceDisplay: store-currency-balance-display-telecrystal
+ priceDisplay: store-currency-price-display-telecrystal
+ entityId: Telecrystal1
+ canWithdraw: true
+
+#debug
+- type: currency
+ id: DebugDollar
+ balanceDisplay: store-currency-balance-display-debugdollar
+ priceDisplay: store-currency-price-display-debugdollar
\ No newline at end of file
diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml
new file mode 100644
index 0000000000..f69bfcef2c
--- /dev/null
+++ b/Resources/Prototypes/Store/presets.yml
@@ -0,0 +1,16 @@
+- type: storePreset
+ id: StorePresetUplink
+ storeName: Uplink
+ categories:
+ - UplinkWeapons
+ - UplinkAmmo
+ - UplinkExplosives
+ - UplinkMisc
+ - UplinkBundles
+ - UplinkTools
+ - UplinkUtility
+ - UplinkJob
+ - UplinkArmor
+ - UplinkPointless
+ currencyWhitelist:
+ - Telecrystal
\ No newline at end of file
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 97768a6690..5a2f7044a2 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -332,6 +332,9 @@
- type: Tag
id: NoSpinOnThrow
+- type: Tag
+ id: NukeOpsUplink
+
- type: Tag
id: Ointment