using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; using Content.Shared.Kitchen.Components; using Content.Shared.Stacks; using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Shared.Kitchen.EntitySystems; public abstract partial class MicrowaveSystem { /// /// This event tries to get secret recipes that the microwave might be capable of. /// Currently, we only check the microwave itself, but in the future, the user might be able to learn recipes. /// [SubscribeLocalEvent] private void OnGetSecretRecipes(Entity ent, ref GetSecretRecipesEvent args) { foreach (var recipeId in ent.Comp.ProvidedRecipes) { if (ProtoMan.Resolve(recipeId, out var recipeProto)) args.Recipes.Add(recipeProto); } } /// /// Adds all usable ingredients from a given entity to an ingredient list. /// /// /// The entity itself is a "solid". /// If it's a stack, then that stack is its "stacks". /// If it has a usable ingredient solution, then the solution's contents are "reagents". /// /// The entity to use as ingredients. /// A dictionary of available ingredients to add to. private void AddItemIngredients(EntityUid item, ref CookingIngredients ingredients) { if (TryGetSolidId(item, out var solidId)) ingredients.AddSolid(solidId.Value); if (StackQuery.TryComp(item, out var stack) && TryGetStackId(item, out var stackId)) ingredients.AddStack(stackId.Value, stack.Count); if (TryGetUsableIngredientSolution(item, out _, out var solution)) { foreach (var (reagent, quantity) in solution.Contents) { ingredients.AddReagent(reagent.Prototype, quantity); } } } /// /// Attempts to get a solution from an entity that can be used as viable ingredients in a recipe. /// /// /// For example, a beaker's contents will work, but not the contents of an uncracked egg. /// /// The entity to attempt to get a usable ingredient solution for. /// A usable solution entity, if available. /// A usable solution, if available. /// Whether or not a usable ingredient solution was successfully retrieved. private bool TryGetUsableIngredientSolution(EntityUid uid, [NotNullWhen(true)] out Entity? solutionEntity, [NotNullWhen(true)] out Solution? solution) { return Solution.TryGetDrainableSolution(uid, out solutionEntity, out solution); } /// /// Given a dictionary of stacks that need to be spent in a recipe, and the amount of stacks /// of a stack we have available, this function gets the number of stacks we need to remove /// from the stack entity. It also removes this amount from the remaining stacks dictionary. /// /// How many stacks we have available. /// The stack ID associated with the stack. /// How many stacks still need to be spent in the recipe. /// How many stacks we should remove from the available stack entity. private static int SpendStackQuantity(int availableStacks, ProtoId stackId, CookingIngredients ingredientsToSpend) { if (!ingredientsToSpend.Stacks.TryGetValue(stackId, out var remaining)) return 0; var spent = Math.Min(availableStacks, remaining); ingredientsToSpend.AddStack(stackId, -spent); return spent; } /// /// Given a dictionary of reagents that need to be spent in a recipe, and a quantity of a reagent /// that we have available in a solution, this function gets the amount of reagents we need to /// remove from the solution. This also removes that amount from the "reagents to spend" dictionary. /// /// How much reagent we have available. /// The ID of the reagent we are spending. /// A dictionary of reagents that still need to be spent. /// How much we should reduce the available reagent volume by. private static FixedPoint2 SpendReagentQuantity(FixedPoint2 availableQuantity, ProtoId reagent, CookingIngredients ingredientsToSpend) { if (!ingredientsToSpend.Reagents.TryGetValue(reagent, out var remaining)) return 0; var spent = FixedPoint2.Min(availableQuantity, remaining); ingredientsToSpend.AddReagent(reagent, -spent); return spent; } /// /// Removes a solid ingredient that is used in a recipe, removing it from the dictionary of /// remaining solids that still need to be spent in the recipe. /// /// The entity to remove. /// The solid ID of the ingredient. /// The microwave's ingredient storage container. /// The struct representing ingredients we still need to spend. private void SubtractSolidContents(EntityUid item, EntProtoId itemProto, Container container, CookingIngredients ingredientsToSpend) { if (!ingredientsToSpend.Solids.ContainsKey(itemProto)) return; ingredientsToSpend.AddSolid(itemProto, -1); Container.Remove(item, container); PredictedQueueDel(item); } /// /// Given a dictionary of remaining stacks that need to be spent in a recipe, this function /// reduces a stack entity's count by however many stacks need to be spent. This also removes the /// stack count from our remaining ingredients. /// /// /// If a recipe calls for two stacks of plasma sheets, and you put 5 sheets in the microwave, then /// the plasma sheet stack would reduce by 2 (leaving you with 3 remaining), and plasma will /// be removed from the remaining stacks dictionary. /// /// The stack entity. /// The struct representing ingredients we still need to spend. private void SubtractStackContents(Entity ent, CookingIngredients ingredientsToSpend) { if (!Resolve(ent.Owner, ref ent.Comp)) return; var stack = ent.Comp; var stackId = stack.StackTypeId; var startingQuantity = stack.Count; var quantityToRemove = SpendStackQuantity(startingQuantity, stackId, ingredientsToSpend); _stack.ReduceCount(ent.AsNullable(), quantityToRemove); } /// /// Given a dictionary of remaining reagents that still need to be spent in a recipe, this function iterates /// over a solution's contents and subtracts reagents according to the reagents to spend. This also removes /// it from our remaining ingredients. /// /// /// Say you still have 6u mayonnaise remaining, and the solution has 10u mayonnaise, then 6u of /// mayonnaise would be removed from the solution, leaving you with 4u left over. Then, mayonnaise /// is removed from the dictionary of remaining reagents. /// /// The solution entity. /// The solution itself. /// The struct representing ingredients we still need to spend. private void SubtractReagentContents(Entity solutionEntity, Solution solution, CookingIngredients ingredientsToSpend) { var reagentsToProcess = ingredientsToSpend.Reagents.Keys.ToList(); foreach (var reagent in reagentsToProcess) { var availableQuantity = solution.GetTotalPrototypeQuantity(reagent); if (availableQuantity == 0) continue; var quantityToRemove = SpendReagentQuantity(availableQuantity, reagent, ingredientsToSpend); Solution.RemoveReagent(solutionEntity, reagent, quantityToRemove); } } /// /// Attempt to get the solid ID of a given entity. /// /// The entity to retrieve a solid ID for. /// The solid ID of the entity, if any. /// /// Whether or not the solid ID was successfully retrieved. False if entity lacks an entity prototype ID. /// // TODO: Solids should be tag-based, or something like that. Not prototype-based. private bool TryGetSolidId(EntityUid item, [NotNullWhen(true)] out EntProtoId? solidId) { solidId = MetaData(item).EntityPrototype?.ID; return solidId != null; } /// /// Attempt to get the stack ID of a given entity. /// /// The stack entity. /// The stack prototype associated with this entity, if any. /// /// Whether or not a stack ID is successfully retrieved. False if this entity is not a stack. /// private bool TryGetStackId(Entity ent, [NotNullWhen(true)] out ProtoId? stack) { stack = null; if (!Resolve(ent.Owner, ref ent.Comp, logMissing: false)) return false; stack = ent.Comp.StackTypeId; return true; } /// /// Spend ingredients in the microwave based on a given recipe. This deletes ingredient entities (solids), /// subtracts respective stack counts, and removes reagents from ingredient containers (reagents). /// /// /// This function does not check whether or not the contents have *enough* ingredients to /// subtract - it simply performs the subtraction. /// /// The microwave entity. /// The recipe used to spend ingredients. private void SubtractContents(Entity ent, PortionedRecipe portionedRecipe) { if (!ProtoMan.Resolve(portionedRecipe.Recipe, out var recipe)) return; var ingredientsToSpend = recipe.Ingredients * portionedRecipe.Count; var solidsToSpend = ingredientsToSpend.Solids; var stacksToSpend = ingredientsToSpend.Stacks; var reagentsToSpend = ingredientsToSpend.Reagents; var microwave = ent.Comp; var microwaveItems = GetMicrowaveContents(ent.AsNullable()).ToArray(); foreach (var item in microwaveItems) { if (solidsToSpend.Count > 0 && TryGetSolidId(item, out var solidId) && solidsToSpend.ContainsKey(solidId.Value)) { SubtractSolidContents(item, solidId.Value, microwave.Storage, ingredientsToSpend); continue; // We're exiting early here; if the solid ingredient is removed from the container, // then we shouldn't be attempting to use its stack or reagents. } if (stacksToSpend.Count > 0 && StackQuery.TryComp(item, out var stack) && TryGetStackId((item, stack), out var stackId) && stacksToSpend.ContainsKey(stackId.Value)) { SubtractStackContents((item, stack), ingredientsToSpend); if (Deleted(item) || stack.Count <= 0) continue; // We're exiting early here - if the stack is empty, then the stack entity // is gonna be deleted. Which means we shouldn't be using its reagents. } if (reagentsToSpend.Count > 0 && TryGetUsableIngredientSolution(item, out var solutionEntity, out var solution) && solution.Volume > 0) SubtractReagentContents(solutionEntity.Value, solution, ingredientsToSpend); } } }