diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index b549918d7c..f87b92bc61 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -1,22 +1,27 @@ using Content.Shared.Store; using JetBrains.Annotations; -using Robust.Client.GameObjects; using System.Linq; -using System.Threading; -using Serilog; -using Timer = Robust.Shared.Timing.Timer; +using Robust.Shared.Prototypes; namespace Content.Client.Store.Ui; [UsedImplicitly] public sealed class StoreBoundUserInterface : BoundUserInterface { + private IPrototypeManager _prototypeManager = default!; + [ViewVariables] private StoreMenu? _menu; [ViewVariables] private string _windowName = Loc.GetString("store-ui-default-title"); + [ViewVariables] + private string _search = ""; + + [ViewVariables] + private HashSet _listings = new(); + public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -49,6 +54,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface SendMessage(new StoreRequestUpdateInterfaceMessage()); }; + _menu.SearchTextUpdated += (_, search) => + { + _search = search.Trim().ToLowerInvariant(); + UpdateListingsWithSearchFilter(); + }; + _menu.OnRefundAttempt += (_) => { SendMessage(new StoreRequestRefundMessage()); @@ -64,10 +75,10 @@ public sealed class StoreBoundUserInterface : BoundUserInterface switch (state) { case StoreUpdateState msg: - _menu.UpdateBalance(msg.Balance); - _menu.PopulateStoreCategoryButtons(msg.Listings); + _listings = msg.Listings; - _menu.UpdateListing(msg.Listings.ToList()); + _menu.UpdateBalance(msg.Balance); + UpdateListingsWithSearchFilter(); _menu.SetFooterVisibility(msg.ShowFooter); _menu.UpdateRefund(msg.AllowRefund); break; @@ -89,4 +100,19 @@ public sealed class StoreBoundUserInterface : BoundUserInterface _menu?.Close(); _menu?.Dispose(); } + + private void UpdateListingsWithSearchFilter() + { + if (_menu == null) + return; + + var filteredListings = new HashSet(_listings); + if (!string.IsNullOrEmpty(_search)) + { + filteredListings.RemoveWhere(listingData => !ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search) && + !ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listingData, _prototypeManager).Trim().ToLowerInvariant().Contains(_search)); + } + _menu.PopulateStoreCategoryButtons(filteredListings); + _menu.UpdateListing(filteredListings.ToList()); + } } diff --git a/Content.Client/Store/Ui/StoreMenu.xaml b/Content.Client/Store/Ui/StoreMenu.xaml index 4b38352a44..fc4cbe444f 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml +++ b/Content.Client/Store/Ui/StoreMenu.xaml @@ -28,7 +28,8 @@ HorizontalAlignment="Right" Text="Refund" /> - + + diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs index 5dc1ab246b..67e5d360a3 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Threading; using Content.Client.Actions; using Content.Client.GameTicking.Managers; using Content.Client.Message; @@ -27,6 +26,7 @@ public sealed partial class StoreMenu : DefaultWindow private StoreWithdrawWindow? _withdrawWindow; + public event EventHandler? SearchTextUpdated; public event Action? OnListingButtonPressed; public event Action? OnCategoryButtonPressed; public event Action? OnWithdrawAttempt; @@ -46,6 +46,7 @@ public sealed partial class StoreMenu : DefaultWindow WithdrawButton.OnButtonDown += OnWithdrawButtonDown; RefreshButton.OnButtonDown += OnRefreshButtonDown; RefundButton.OnButtonDown += OnRefundButtonDown; + SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text); if (Window != null) Window.Title = name; @@ -59,7 +60,7 @@ public sealed partial class StoreMenu : DefaultWindow (type.Key, type.Value), type => _prototypeManager.Index(type.Key)); var balanceStr = string.Empty; - foreach (var ((type, amount),proto) in currency) + foreach (var ((_, amount), proto) in currency) { balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount), ("currency", Loc.GetString(proto.DisplayName, ("amount", 1)))); @@ -81,7 +82,6 @@ public sealed partial class StoreMenu : DefaultWindow { 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(); @@ -129,8 +129,8 @@ public sealed partial class StoreMenu : DefaultWindow if (!listing.Categories.Contains(CurrentCategory)) return; - var listingName = Loc.GetString(listing.Name); - var listingDesc = Loc.GetString(listing.Description); + var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager); + var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager); var listingPrice = listing.Cost; var canBuy = CanBuyListing(Balance, listingPrice); @@ -144,12 +144,6 @@ public sealed partial class StoreMenu : DefaultWindow { 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) { @@ -243,13 +237,16 @@ public sealed partial class StoreMenu : DefaultWindow allCategories = allCategories.OrderBy(c => c.Priority).ToList(); + // This will reset the Current Category selection if nothing matches the search. + if (allCategories.All(category => category.ID != CurrentCategory)) + CurrentCategory = string.Empty; + if (CurrentCategory == string.Empty && allCategories.Count > 0) CurrentCategory = allCategories.First().ID; - if (allCategories.Count <= 1) - return; - CategoryListContainer.Children.Clear(); + if (allCategories.Count < 1) + return; foreach (var proto in allCategories) { diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 49db980451..e6c4eb0cce 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -15,6 +15,7 @@ using Content.Shared.UserInterface; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; +using Robust.Shared.Prototypes; namespace Content.Server.Store.Systems; @@ -29,6 +30,7 @@ public sealed partial class StoreSystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; private void InitializeUi() { @@ -259,7 +261,7 @@ public sealed partial class StoreSystem //log dat shit. _admin.Add(LogType.StorePurchase, LogImpact.Low, - $"{ToPrettyString(buyer):player} purchased listing \"{Loc.GetString(listing.Name)}\" from {ToPrettyString(uid)}"); + $"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}"); listing.PurchaseAmount++; //track how many times something has been purchased _audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching! diff --git a/Content.Shared/Store/ListingLocalisationHelpers.cs b/Content.Shared/Store/ListingLocalisationHelpers.cs new file mode 100644 index 0000000000..3ac75cd801 --- /dev/null +++ b/Content.Shared/Store/ListingLocalisationHelpers.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Store; + +public static class ListingLocalisationHelpers +{ + /// + /// ListingData's Name field can be either a localisation string or the actual entity's name. + /// This function gets a localised name from the localisation string if it exists, and if not, it gets the entity's name. + /// If neither a localised string exists, or an associated entity name, it will return the value of the "Name" field. + /// + public static string GetLocalisedNameOrEntityName(ListingData listingData, IPrototypeManager prototypeManager) + { + bool wasLocalised = Loc.TryGetString(listingData.Name, out string? listingName); + + if (!wasLocalised && listingData.ProductEntity != null) + { + var proto = prototypeManager.Index(listingData.ProductEntity); + listingName = proto.Name; + } + + return listingName ?? listingData.Name; + } + + /// + /// ListingData's Description field can be either a localisation string or the actual entity's description. + /// This function gets a localised description from the localisation string if it exists, and if not, it gets the entity's description. + /// If neither a localised string exists, or an associated entity description, it will return the value of the "Description" field. + /// + public static string GetLocalisedDescriptionOrEntityDescription(ListingData listingData, IPrototypeManager prototypeManager) + { + bool wasLocalised = Loc.TryGetString(listingData.Description, out string? listingDesc); + + if (!wasLocalised && listingData.ProductEntity != null) + { + var proto = prototypeManager.Index(listingData.ProductEntity); + listingDesc = proto.Description; + } + + return listingDesc ?? listingData.Description; + } +}