using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Emag.Systems; using Content.Shared.Examine; using Content.Shared.Lathe.Prototypes; using Content.Shared.Localizations; using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Lathe; /// /// This handles... /// public abstract partial class SharedLatheSystem : EntitySystem { [Dependency] private SharedMaterialStorageSystem _materialStorage = default!; [Dependency] private EmagSystem _emag = default!; public readonly Dictionary> InverseRecipes = new(); public const int MaxItemsPerRequest = 10_000; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnPrototypesReloaded); BuildInverseRecipeDictionary(); } /// /// Get the set of all recipes that a lathe could possibly ever create (e.g., if all techs were unlocked). /// public HashSet> GetAllPossibleRecipes(LatheComponent component) { var recipes = new HashSet>(); foreach (var pack in component.StaticPacks) { recipes.UnionWith(ProtoMan.Index(pack).Recipes); } foreach (var pack in component.DynamicPacks) { recipes.UnionWith(ProtoMan.Index(pack).Recipes); } return recipes; } /// /// Add every recipe in the list of recipe packs to a single hashset. /// public void AddRecipesFromPacks(HashSet> recipes, IEnumerable> packs) { foreach (var id in packs) { var pack = ProtoMan.Index(id); recipes.UnionWith(pack.Recipes); } } private void OnExamined(Entity ent, ref ExaminedEvent args) { if (!args.IsInDetailsRange) return; if (ent.Comp.ReagentOutputSlotId != null) args.PushMarkup(Loc.GetString("lathe-menu-reagent-slot-examine")); } [PublicAPI] public bool CanProduce(EntityUid uid, string recipe, int amount = 1, LatheComponent? component = null) { return ProtoMan.TryIndex(recipe, out var proto) && CanProduce(uid, proto, amount, component); } public bool CanProduce(EntityUid uid, LatheRecipePrototype recipe, int amount = 1, LatheComponent? component = null) { if (!Resolve(uid, ref component)) return false; if (!HasRecipe(uid, recipe, component)) return false; if (amount <= 0) return false; var materials = _materialStorage.GetStoredMaterials(uid); return HasMaterials(materials, recipe, component.MaterialUseMultiplier, amount); } /// /// Returns whether or not the given lathe can produce some number of a given recipe. /// /// /// Useful for reducing material lookup with batched checks. /// /// The lathe that would produce the recipe. /// The recipe to be produced. /// The set of materials to check. /// The number of times the recipe should be made. public bool CanProduce(Entity ent, LatheRecipePrototype recipe, Dictionary, int> materials, int amount) { if (!Resolve(ent, ref ent.Comp)) return false; if (amount <= 0) return false; if (!HasRecipe(ent, recipe, ent.Comp)) return false; return HasMaterials(materials, recipe, ent.Comp.MaterialUseMultiplier, amount); } /// /// Returns whether or not the given materials dictionary can produce copies of . /// private bool HasMaterials(Dictionary, int> materials, LatheRecipePrototype recipe, float materialMultiplier, int amount = 1) { foreach (var (material, needed) in recipe.Materials) { if (!materials.TryGetValue(material, out var availableAmount)) return false; var adjustedAmount = AdjustMaterial(needed, recipe.ApplyMaterialDiscount, materialMultiplier); if (availableAmount < adjustedAmount * amount) return false; } return true; } private void OnEmagged(EntityUid uid, EmagLatheRecipesComponent component, ref GotEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) return; if (_emag.CheckFlag(uid, EmagType.Interaction)) return; args.Handled = true; } public static int AdjustMaterial(int original, bool reduce, float multiplier) => reduce ? (int)MathF.Ceiling(original * multiplier) : original; protected abstract bool HasRecipe(EntityUid uid, LatheRecipePrototype recipe, LatheComponent component); private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj) { if (!obj.WasModified()) return; BuildInverseRecipeDictionary(); } private void BuildInverseRecipeDictionary() { InverseRecipes.Clear(); foreach (var latheRecipe in ProtoMan.EnumeratePrototypes()) { if (latheRecipe.Result is not { } result) continue; InverseRecipes.GetOrNew(result).Add(latheRecipe); } } public bool TryGetRecipesFromEntity(string prototype, [NotNullWhen(true)] out List? recipes) { recipes = new(); if (InverseRecipes.TryGetValue(prototype, out var r)) recipes.AddRange(r); return recipes.Count != 0; } public string GetRecipeName(ProtoId proto) { return GetRecipeName(ProtoMan.Index(proto)); } public string GetRecipeName(LatheRecipePrototype proto) { if (!string.IsNullOrWhiteSpace(proto.Name)) return Loc.GetString(proto.Name); if (proto.Result is { } result) { return ProtoMan.Index(result).Name; } if (proto.ResultReagents is { } resultReagents) { return ContentLocalizationManager.FormatList(resultReagents .Select(p => Loc.GetString("lathe-menu-result-reagent-display", ("reagent", ProtoMan.Index(p.Key).LocalizedName), ("amount", p.Value))) .ToList()); } return string.Empty; } [PublicAPI] public string GetRecipeDescription(ProtoId proto) { return GetRecipeDescription(ProtoMan.Index(proto)); } public string GetRecipeDescription(LatheRecipePrototype proto) { if (!string.IsNullOrWhiteSpace(proto.Description)) return Loc.GetString(proto.Description); if (proto.Result is { } result) { return ProtoMan.Index(result).Description; } if (proto.ResultReagents is { } resultReagents) { // We only use the first one for the description since these descriptions don't combine very well. var reagent = resultReagents.First().Key; return ProtoMan.Index(reagent).LocalizedDescription; } return string.Empty; } }