CatStation/Content.Shared/Kitchen/EntitySystems/MicrowaveSystem.Recipes.cs
portfiend b9b794deef
Microwave Refactor and Prediction (#43129)
* adjust fields of meal recipe prototype

* document microwave recipe datafields

* use file scoped namespace for microwave recipes

* more of this

* localize every microwave recipe

* make microwave system shared

* refactor recipe reagent contents

* refactor recipe solids + add material ingredient datafield

* im making this a bit less silly

* awa

* move this shit out

* move this shit out also

* refactor Wzhzhzh

* im gonna shorten this parameter by 1 actually

* move this into recipes

* make processcontents a little better(?)

* clean imports

* rename a struct

* use cooking ingredients struct

* comment

* use whitelist for this

* unused dependency

* rearranged the parameters of get recipe portions

* rearrange some stuff

* clean up more imports

* move container related functions into separate file

* move this function out of ProcessContents

* prevent microwaves from having items inserted while active

* use IsPowered in microwave system

* use byref syntax and Entity<>

* moving the override up
sorry i like it being up here

* refactor microwave updating

* clean up recipe finish logic

* im renaming this function actually

* move some functions around

* move more stuff around

* some comments

* fix compiler error

* fix the yml of all microwave recipes

* use materials instead of solids for this stuff

* oops

* fix this

* turn microwave sparks into datafield

* move activelymicrowaved events into separate folder

* make UpdateUserInterfaceState use entity only

* rearrange some functions

* move UI functions out

* comment microwave UI stuff

* more comments

* comment recipe functions

* refactor GetTotalIngredients to use an operator

* give CookingIngredients addition methods

* comment the rest of CookingIngredients

* make subtraction functions clearer?

* remove redundant comment

* handle zero/negative ingredient cases

* simplify subtract contents logic

* fix these comments

* typo

* add microwave recipe test

* move active microwave methods

* comment active microwave methods

* comment cooking methods

* comment microwave methods

* change setappearance to use entity<>

* add some public api decorators

* comment all of MicrowaveComponent

* use file-scoped namespace for MicrowaveComponent

* comment

* fix baguette sword

* maybe...?

* oh oops

* document the rest of the owl

* kill the massive YML breaking change

* fix protoman dependency

* move shared microwave stuff to SharedMicrowaveSystem

* fix usings

* move microwave components to shared

* fix up microwave datafields

* move APIs to shared

* move activemicrowave methods to shared

* initialize on shared

* move ui functions to shared

* add actor here

* stuff

* predict microwave update loop

* make clientside microwave system

* adjust the update loop for microwaves

* clean up

* add field deltas to microwaves

* dirty fields instead of whole component

* move this to the partial

* move GetRecipesForMicrowave to shared

* comments

* predict microwave BUI

* oops

* refactor microwave bui update

* move UI messages to shared

* move messages to UI file

* move init to shared

* move container methosd to shared

* oops my using

* move cooking functions into partial

* move cooking methods to shared

* move microwave recipe methods to shared

* move the rest of the microwave events to shared

* update imports

* remove custom type serializer

* better (?) microwave UI prediction

* fix list notations

* OK IT WAS THE DATAFIELDS

* add IOC inject

* dont use IOC for htis

* sealed

* prediction fixes pt1

* make microwave cooking updates more consistent

* datafield tweaks

* test for multiple portions in microwave recipes

* test for multiple recipe portions

* better(?) prediction for malfunctions

* remove redundant comment

* prevent solution reactions in microwave

* cleanup some microwave stuff

* eh i think this should be container actually

* this can be API i think

* reduce system redundancy

* this has to be public

* use resolve pattern for explode

* use GetMicrowaveContents in microwave system

* this can be an API too

* add conversion script for unlocalized recipes

* use EnsureComp instead of AddComp

* suffering

* fix recipe prediction

* fix the audio

* reduce redundant networking

* i think this should be autopaused?

* nevermind we use dynamic portions in the test

* oops i wrote the test wrong

* make lemoon cake take 2 lemoons again
sorry test fail

* rename "materials" to "stacks"
realized theyre different

* add stack guidebook quantities

* Kill audio

* fix lemoon cake recipe for real?

* mark PortionedRecipe as a datadefinition

* fix the microwave timer

* review

* cleanup

* Update SharedMicrowaveSystem.Recipes.cs

* update

* add audio params

* review: inject microwavesystem

* review: named variable

* review: move dependencies

* review: use gametest for recipe test

* review: make access attributes standalone

* rename prototypes and datafields

* review: rename this var

* review: move this function down

* review: remove these scripts

* actually this should stay

* refactor cook time buttons

* review: use a color name for this

* fix timer reset

* Dirty

* review: make ActivelyMicrowavedComponent networked

* review: slarti comments part 1

* review: remove serializable attribute from CookingIngredients

* rename file

* review: more microwave audio boilerplate. yippee!

* review: use resolve pattern for TryGetStackId
i dont like the way this makes the rest of the code look but we ball maybe. i dont know what else to do here

* review: move ui update to constructor

* review: kill LastCookUpdated

* actually i do this instead

* fixed up the PR

* cleanup

* ///

* Update microwave.yml

---------

Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
2026-10-04 23:38:48 +00:00

282 lines
12 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
[SubscribeLocalEvent]
private void OnGetSecretRecipes(Entity<FoodRecipeProviderComponent> ent, ref GetSecretRecipesEvent args)
{
foreach (var recipeId in ent.Comp.ProvidedRecipes)
{
if (ProtoMan.Resolve(recipeId, out var recipeProto))
args.Recipes.Add(recipeProto);
}
}
/// <summary>
/// Adds all usable ingredients from a given entity to an ingredient list.
/// </summary>
/// <remarks>
/// 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".
/// </remarks>
/// <param name="item">The entity to use as ingredients.</param>
/// <param name="ingredients">A dictionary of available ingredients to add to.</param>
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);
}
}
}
/// <summary>
/// Attempts to get a solution from an entity that can be used as viable ingredients in a recipe.
/// </summary>
/// <remarks>
/// For example, a beaker's contents will work, but not the contents of an uncracked egg.
/// </remarks>
/// <param name="uid">The entity to attempt to get a usable ingredient solution for.</param>
/// <param name="solutionEntity">A usable solution entity, if available.</param>
/// <param name="solution">A usable solution, if available.</param>
/// <returns>Whether or not a usable ingredient solution was successfully retrieved.</returns>
private bool TryGetUsableIngredientSolution(EntityUid uid,
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
[NotNullWhen(true)] out Solution? solution)
{
return Solution.TryGetDrainableSolution(uid, out solutionEntity, out solution);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="availableStacks">How many stacks we have available.</param>
/// <param name="stackId">The stack ID associated with the stack.</param>
/// <param name="ingredientsToSpend">How many stacks still need to be spent in the recipe.</param>
/// <returns>How many stacks we should remove from the available stack entity.</returns>
private static int SpendStackQuantity(int availableStacks,
ProtoId<StackPrototype> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="availableQuantity">How much reagent we have available.</param>
/// <param name="reagent">The ID of the reagent we are spending.</param>
/// <param name="ingredientsToSpend">A dictionary of reagents that still need to be spent.</param>
/// <returns>How much we should reduce the available reagent volume by.</returns>
private static FixedPoint2 SpendReagentQuantity(FixedPoint2 availableQuantity,
ProtoId<ReagentPrototype> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="item">The entity to remove.</param>
/// <param name="itemProto">The solid ID of the ingredient.</param>
/// <param name="container">The microwave's ingredient storage container.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="ent">The stack entity.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
private void SubtractStackContents(Entity<StackComponent?> 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);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="solutionEntity">The solution entity.</param>
/// <param name="solution">The solution itself.</param>
/// <param name="ingredientsToSpend">The struct representing ingredients we still need to spend.</param>
private void SubtractReagentContents(Entity<SolutionComponent> 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);
}
}
/// <summary>
/// Attempt to get the solid ID of a given entity.
/// </summary>
/// <param name="item">The entity to retrieve a solid ID for.</param>
/// <param name="solidId">The solid ID of the entity, if any.</param>
/// <returns>
/// Whether or not the solid ID was successfully retrieved. False if entity lacks an entity prototype ID.
/// </returns>
// 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;
}
/// <summary>
/// Attempt to get the stack ID of a given entity.
/// </summary>
/// <param name="ent">The stack entity.</param>
/// <param name="stack">The stack prototype associated with this entity, if any.</param>
/// <returns>
/// Whether or not a stack ID is successfully retrieved. False if this entity is not a stack.
/// </returns>
private bool TryGetStackId(Entity<StackComponent?> ent,
[NotNullWhen(true)] out ProtoId<StackPrototype>? stack)
{
stack = null;
if (!Resolve(ent.Owner, ref ent.Comp, logMissing: false))
return false;
stack = ent.Comp.StackTypeId;
return true;
}
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// This function does not check whether or not the contents have *enough* ingredients to
/// subtract - it simply performs the subtraction.
/// </remarks>
/// <param name="ent">The microwave entity.</param>
/// <param name="portionedRecipe">The recipe used to spend ingredients.</param>
private void SubtractContents(Entity<MicrowaveComponent> 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);
}
}
}