// SPDX-FileCopyrightText: 2025 GoobBot // SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org> // SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com> // // SPDX-License-Identifier: AGPL-3.0-or-later using Content.Client.Construction; using Content.Client.Construction.UI; using Content.Shared._Goobstation.Factory; using Content.Shared.Construction.Prototypes; using Content.Shared.Whitelist; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Prototypes; using System.Linq; namespace Content.Client._Goobstation.Factory.UI; public sealed class ConstructorBUI : BoundUserInterface { [Dependency] private readonly IPrototypeManager _proto = default!; private readonly ConstructionSystem _construction; private readonly EntityWhitelistSystem _whitelist; private readonly SpriteSystem _sprite; private ConstructionMenu? _menu; private string? _id; private List _recipes = new(); private readonly LocId _favoriteCatName = "construction-category-favorites"; private readonly LocId _forAllCategoryName = "construction-category-all"; public ConstructorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey) { _construction = EntMan.System(); _whitelist = EntMan.System(); _sprite = EntMan.System(); _id = EntMan.GetComponentOrNull(owner)?.Construction; } protected override void Open() { base.Open(); // god BLESS whoever made construction ui for having it so decoupled <3 _menu = this.CreateWindow(); PopulateCategories(); PopulateRecipes(string.Empty, string.Empty); _menu.PopulateRecipes += (_, args) => PopulateRecipes(args.Item1, args.Item2); _menu.RecipeSelected += (_, item) => { _menu.ClearRecipeInfo(); if (item != null && item.Prototype != null) { _id = item.Prototype.ID; _menu.SetRecipeInfo(item.Prototype.Name ?? "", item.Prototype.Description ?? "", item?.TargetPrototype, item!.Prototype.Type != ConstructionType.Item, true); // TODO: favourites GenerateStepList(item.Prototype); } else { _id = null; } }; _menu.BuildButtonToggled += (_, _) => { SendPredictedMessage(new ConstructorSetProtoMessage(_id)); _menu.Close(); }; } private void PopulateCategories(string? selected = null) { if (_menu is not {} menu) return; var categories = new HashSet(); foreach (var prototype in _proto.EnumeratePrototypes()) { var category = prototype.Category; if (!string.IsNullOrEmpty(category)) categories.Add(category); } var categoriesArray = new string[categories.Count + 1]; // hard-coded to show all recipes var idx = 0; categoriesArray[idx++] = _forAllCategoryName; foreach (var cat in categories.OrderBy(Loc.GetString)) { categoriesArray[idx++] = cat; } menu.OptionCategories.Clear(); for (var i = 0; i < categoriesArray.Length; i++) { menu.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i); if (!string.IsNullOrEmpty(selected) && selected == categoriesArray[i]) menu.OptionCategories.SelectId(i); } menu.Categories = categoriesArray; } // copypasted and optimised from ConstructionMenuPresenter private void PopulateRecipes(string search, string category) { if (PlayerManager.LocalEntity is not { } user || _menu is not { } menu) return; search = search.Trim().ToLowerInvariant(); var searching = !string.IsNullOrEmpty(search); var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName; _recipes.Clear(); foreach (var recipe in _proto.EnumeratePrototypes()) { if (recipe.Hide) continue; if (_whitelist.IsWhitelistFail(recipe.EntityWhitelist, user)) continue; if (searching && recipe.Name != null && !recipe.Name.ToLowerInvariant().Contains(search)) continue; if (!isEmptyCategory) { // TODO: when favourites get sent from server do this // currently its specific to the G menu //if (!_favoritedRecipes.Contains(recipe)) if (category == _favoriteCatName) continue; else if (recipe.Category != category) continue; } if (!_construction!.TryGetRecipePrototype(recipe.ID, out var targetProtoId)) continue; if (!_proto.TryIndex(targetProtoId, out EntityPrototype? proto)) continue; _recipes.Add(new(recipe, proto)); } _recipes.Sort((a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture)); var recipesList = menu.Recipes; recipesList.PopulateList(_recipes); menu.RecipesGridScrollContainer.Visible = false; menu.Recipes.Visible = true; } private void GenerateStepList(ConstructionPrototype proto) { if (_construction.GetGuide(proto) is not { } guide || _menu is not { } menu) return; var list = menu.RecipeStepList; foreach (var entry in guide.Entries) { var text = entry.Arguments != null ? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization); if (entry.EntryNumber is { } number) text = Loc.GetString("construction-presenter-step-wrapper", ("step-number", number), ("text", text)); // The padding needs to be applied regardless of text length... (See PadLeft documentation) text = text.PadLeft(text.Length + entry.Padding); var icon = entry.Icon != null ? _sprite.Frame0(entry.Icon) : Texture.Transparent; list.AddItem(text, icon, false); } } }