Files
tbd-station-14/Content.Client/Lathe/UI/LatheMenu.xaml.cs
Pieter-Jan Briers 0c97520276 Fix usages of TryIndex() (#39124)
* Fix usages of TryIndex()

Most usages of TryIndex() were using it incorrectly. Checking whether prototype IDs specified in prototypes actually existed before using them. This is not appropriate as it's just hiding bugs that should be getting caught by the YAML linter and other tools. (#39115)

This then resulted in TryIndex() getting modified to log errors (94f98073b0), which is incorrect as it causes false-positive errors in proper uses of the API: external data validation. (#39098)

This commit goes through and checks every call site of TryIndex() to see whether they were correct. Most call sites were replaced with the new Resolve(), which is suitable for these "defensive programming" use cases.

Fixes #39115

Breaking change: while doing this I noticed IdCardComponent and related systems were erroneously using ProtoId<AccessLevelPrototype> for job prototypes. This has been corrected.

* fix tests

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-09-09 18:17:56 +02:00

365 lines
12 KiB
C#

using System.Linq;
using System.Text;
using Content.Client.Materials;
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
[GenerateTypedNameReferences]
public sealed partial class LatheMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly LatheSystem _lathe;
private readonly MaterialStorageSystem _materialStorage;
public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
public event Action<string, int>? RecipeQueueAction;
public event Action<int>? QueueDeleteAction;
public event Action<int>? QueueMoveUpAction;
public event Action<int>? QueueMoveDownAction;
public event Action? DeleteFabricatingAction;
public List<ProtoId<LatheRecipePrototype>> Recipes = new();
public List<ProtoId<LatheCategoryPrototype>>? Categories;
public ProtoId<LatheCategoryPrototype>? CurrentCategory;
public EntityUid Entity;
public LatheMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entityManager.System<SpriteSystem>();
_lathe = _entityManager.System<LatheSystem>();
_materialStorage = _entityManager.System<MaterialStorageSystem>();
SearchBar.OnTextChanged += _ =>
{
PopulateRecipes();
};
AmountLineEdit.OnTextChanged += _ =>
{
if (int.TryParse(AmountLineEdit.Text, out var amount))
{
if (amount > LatheSystem.MaxItemsPerRequest)
AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString();
else if (amount < 0)
AmountLineEdit.Text = "0";
}
PopulateRecipes();
};
FilterOption.OnItemSelected += OnItemSelected;
ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke();
}
public void SetEntity(EntityUid uid)
{
Entity = uid;
if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComponent))
{
if (!latheComponent.DynamicPacks.Any())
{
ServerListButton.Visible = false;
}
AmountLineEdit.SetText(latheComponent.DefaultProductionAmount.ToString());
}
MaterialsList.SetOwner(Entity);
}
/// <summary>
/// Populates the list of all the recipes
/// </summary>
public void PopulateRecipes()
{
var recipesToShow = new List<LatheRecipePrototype>();
foreach (var recipe in Recipes)
{
if (!_prototypeManager.Resolve(recipe, out var proto))
continue;
// Category filtering
if (CurrentCategory != null)
{
if (proto.Categories.Count <= 0)
continue;
var validRecipe = proto.Categories.Any(category => category == CurrentCategory);
if (!validRecipe)
continue;
}
if (SearchBar.Text.Trim().Length != 0)
{
if (_lathe.GetRecipeName(recipe).ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant()))
recipesToShow.Add(proto);
}
else
{
recipesToShow.Add(proto);
}
}
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
quantity = 1;
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
// Get the existing list of queue controls
var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
int idx = 0;
foreach (var prototype in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
var tooltipFunction = () => GenerateTooltipText(prototype);
if (idx >= oldChildCount)
{
var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
amount = 1;
RecipeQueueAction?.Invoke(s, amount);
};
RecipeList.AddChild(control);
}
else
{
var child = RecipeList.GetChild(idx) as RecipeControl;
if (child == null)
{
DebugTools.Assert($"Lathe menu recipe control at {idx} is not of type RecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetRecipe(prototype);
child.SetTooltipSupplier(tooltipFunction);
child.SetCanProduce(canProduce);
child.SetDisplayControl(GetRecipeDisplayControl(prototype));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
RecipeList.RemoveChild(childIdx);
}
}
private string GenerateTooltipText(LatheRecipePrototype prototype)
{
StringBuilder sb = new();
var multiplier = _entityManager.GetComponent<LatheComponent>(Entity).MaterialUseMultiplier;
foreach (var (id, amount) in prototype.Materials)
{
if (!_prototypeManager.Resolve(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier);
var sheetVolume = _materialStorage.GetSheetVolume(proto);
var unit = Loc.GetString(proto.Unit);
var sheets = adjustedAmount / (float) sheetVolume;
var availableAmount = _materialStorage.GetMaterialAmount(Entity, id);
var missingAmount = Math.Max(0, adjustedAmount - availableAmount);
var missingSheets = missingAmount / (float) sheetVolume;
var name = Loc.GetString(proto.Name);
string tooltipText;
if (missingSheets > 0)
{
tooltipText = Loc.GetString("lathe-menu-material-amount-missing", ("amount", sheets), ("missingAmount", missingSheets), ("unit", unit), ("material", name));
}
else
{
var amountText = Loc.GetString("lathe-menu-material-amount", ("amount", sheets), ("unit", unit));
tooltipText = Loc.GetString("lathe-menu-tooltip-display", ("material", name), ("amount", amountText));
}
sb.AppendLine(tooltipText);
}
var desc = _lathe.GetRecipeDescription(prototype);
if (!string.IsNullOrWhiteSpace(desc))
sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", desc)));
// Remove last newline
if (sb.Length > 0)
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}
public void UpdateCategories()
{
// Get categories from recipes
var currentCategories = new List<ProtoId<LatheCategoryPrototype>>();
foreach (var recipeId in Recipes)
{
var recipe = _prototypeManager.Index(recipeId);
if (recipe.Categories.Count <= 0)
continue;
foreach (var category in recipe.Categories)
{
if (currentCategories.Contains(category))
continue;
currentCategories.Add(category);
}
}
if (Categories != null && (Categories.Count == currentCategories.Count || !Categories.All(currentCategories.Contains)))
return;
Categories = currentCategories;
var sortedCategories = currentCategories
.Select(p => _prototypeManager.Index(p))
.OrderBy(p => Loc.GetString(p.Name))
.ToList();
FilterOption.Clear();
FilterOption.AddItem(Loc.GetString("lathe-menu-category-all"), -1);
foreach (var category in sortedCategories)
{
FilterOption.AddItem(Loc.GetString(category.Name), Categories.IndexOf(category.ID));
}
FilterOption.SelectId(-1);
}
/// <summary>
/// Populates the build queue list with all queued items
/// </summary>
/// <param name="queue"></param>
public void PopulateQueueList(IReadOnlyCollection<LatheRecipeBatch> queue)
{
// Get the existing list of queue controls
var oldChildCount = QueueList.ChildCount;
var idx = 0;
foreach (var batch in queue)
{
var recipe = _prototypeManager.Index(batch.Recipe);
var itemName = _lathe.GetRecipeName(batch.Recipe);
string displayText;
if (batch.ItemsRequested > 1)
displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx + 1), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
else
displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
if (idx >= oldChildCount)
{
var queuedRecipeBox = new QueuedRecipeControl(displayText, idx, GetRecipeDisplayControl(recipe));
queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
QueueList.AddChild(queuedRecipeBox);
}
else
{
var child = QueueList.GetChild(idx) as QueuedRecipeControl;
if (child == null)
{
DebugTools.Assert($"Lathe menu queued recipe control at {idx} is not of type QueuedRecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetDisplayText(displayText);
child.SetIndex(idx);
child.SetDisplayControl(GetRecipeDisplayControl(recipe));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
QueueList.RemoveChild(childIdx);
}
}
public void SetQueueInfo(ProtoId<LatheRecipePrototype>? recipeProto)
{
FabricatingContainer.Visible = recipeProto != null;
if (recipeProto == null)
return;
var recipe = _prototypeManager.Index(recipeProto.Value);
FabricatingDisplayContainer.Children.Clear();
FabricatingDisplayContainer.AddChild(GetRecipeDisplayControl(recipe));
NameLabel.Text = _lathe.GetRecipeName(recipe);
}
public Control GetRecipeDisplayControl(LatheRecipePrototype recipe)
{
if (recipe.Icon != null)
{
var textRect = new TextureRect();
textRect.Texture = _spriteSystem.Frame0(recipe.Icon);
return textRect;
}
if (recipe.Result is { } result)
{
var entProtoView = new EntityPrototypeView();
entProtoView.SetPrototype(result);
return entProtoView;
}
return new Control();
}
private void OnItemSelected(OptionButton.ItemSelectedEventArgs obj)
{
FilterOption.SelectId(obj.Id);
if (obj.Id == -1)
{
CurrentCategory = null;
}
else
{
CurrentCategory = Categories?[obj.Id];
}
PopulateRecipes();
}
}