/* * This file is sublicensed under MIT License * https://github.com/space-wizards/space-station-14/blob/master/LICENSE.TXT */ using System.Linq; using System.Numerics; using Content.Shared._CP14.Cooking.Components; using Content.Shared._CP14.Cooking.Prototypes; using Content.Shared.Audio; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Fluids; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; using Content.Shared.Tag; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Shared._CP14.Cooking; public abstract partial class CP14SharedCookingSystem : EntitySystem { [Dependency] protected readonly SharedContainerSystem _container = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] protected readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPuddleSystem _puddle = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; /// /// When overcooking food, we will replace the reagents inside with this reagent. /// private readonly ProtoId _burntFoodReagent = "CP14BurntFood"; /// /// Stores a list of all recipes sorted by complexity: the most complex ones at the beginning. /// When attempting to cook, the most complex recipes will be checked first, /// gradually moving down to the easiest ones. /// The easiest recipes are usually the most “abstract,” /// so they will be suitable for the largest number of recipes. /// protected List OrderedRecipes = []; public override void Initialize() { base.Initialize(); InitTransfer(); InitDoAfter(); CacheAndOrderRecipes(); SubscribeLocalEvent(OnPrototypesReloaded); SubscribeLocalEvent(OnExaminedEvent); } public override void Update(float frameTime) { UpdateDoAfter(frameTime); } private void CacheAndOrderRecipes() { OrderedRecipes = _proto.EnumeratePrototypes() .Where(recipe => recipe.Requirements.Count > 0) // Only include recipes with requirements .OrderByDescending(recipe => recipe.Requirements.Sum(condition => condition.GetComplexity())) .ToList(); } private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev) { if (!ev.WasModified()) return; CacheAndOrderRecipes(); } private void OnExaminedEvent(Entity ent, ref ExaminedEvent args) { if (ent.Comp.FoodData?.Name is null) return; if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out _, out var solution)) return; if (solution.Volume == 0) return; var remaining = solution.Volume; args.PushMarkup(Loc.GetString("cp14-cooking-examine", ("name", Loc.GetString(ent.Comp.FoodData.Name)), ("count", remaining))); } /// /// Transfer food data from cooker to holder /// protected virtual bool TryTransferFood(Entity target, Entity source) { if (!source.Comp.CanGiveFood || !target.Comp.CanAcceptFood) return false; if (target.Comp.FoodType != source.Comp.FoodType) return false; if (source.Comp.FoodData is null) return false; if (!TryComp(target, out var holderFoodComp)) return false; if (!_solution.TryGetSolution(source.Owner, source.Comp.SolutionId, out var cookerSoln, out var cookerSolution)) return false; //Solutions if (_solution.TryGetSolution(target.Owner, holderFoodComp.Solution, out var holderSoln, out var solution)) { if (solution.Volume > 0) { _popup.PopupEntity(Loc.GetString("cp14-cooking-popup-not-empty", ("name", MetaData(target).EntityName)), target); return false; } _solution.TryTransferSolution(holderSoln.Value, cookerSolution, solution.MaxVolume); } //Trash //If we have a lot of trash, we put 1 random trash in each plate. If it's a last plate (out of solution in cooker), we put all the remaining trash in it. if (source.Comp.FoodData?.Trash.Count > 0) { if (cookerSolution.Volume <= 0) { holderFoodComp.Trash.AddRange(source.Comp.FoodData.Trash); } else { if (_net.IsServer) { var newTrash = _random.Pick(source.Comp.FoodData.Trash); source.Comp.FoodData.Trash.Remove(newTrash); holderFoodComp.Trash.Add(newTrash); } } } if (source.Comp.FoodData is not null) UpdateFoodDataVisuals(target, source.Comp.FoodData); Dirty(target); Dirty(source); _solution.UpdateChemicals(cookerSoln.Value); return true; } private void UpdateFoodDataVisuals( Entity ent, bool rename = true) { var data = ent.Comp.FoodData; if (data is null) return; UpdateFoodDataVisuals(ent, data, rename); } protected virtual void UpdateFoodDataVisuals( Entity ent, CP14FoodData data, bool rename = true) { //Name and Description if (rename) { if (data.Name is not null) _metaData.SetEntityName(ent, Loc.GetString(data.Name)); if (data.Desc is not null) _metaData.SetEntityDescription(ent, Loc.GetString(data.Desc)); } //Flavors EnsureComp(ent, out var flavorComp); foreach (var flavor in data.Flavors) { flavorComp.Flavors.Add(flavor); } //Visuals ent.Comp.FoodData = new CP14FoodData(data); foreach (var layer in data.Visuals) { if (_random.Prob(0.5f)) layer.Scale = new Vector2(-1, 1); } DirtyField(ent, ent.Comp, nameof(CP14FoodHolderComponent.FoodData)); //Sliceable // > on server overrided side } public CP14CookingRecipePrototype? GetRecipe(Entity ent) { if (!_container.TryGetContainer(ent, ent.Comp.ContainerId, out var container)) return null; _solution.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out _, out var solution); //Get all tags var allTags = new List>(); foreach (var contained in container.ContainedEntities) { if (!TryComp(contained, out var tags)) continue; allTags.AddRange(tags.Tags); } return GetRecipe(ent.Comp.FoodType, solution, allTags); } public CP14CookingRecipePrototype? GetRecipe(ProtoId foodType, Solution? solution, List> allTags) { if (OrderedRecipes.Count == 0) { throw new InvalidOperationException( "No cooking recipes found. Please ensure that the CP14CookingRecipePrototype is defined and loaded."); } CP14CookingRecipePrototype? selectedRecipe = null; foreach (var recipe in OrderedRecipes) { if (recipe.FoodType != foodType) continue; var conditionsMet = true; foreach (var condition in recipe.Requirements) { if (!condition.CheckRequirement(EntityManager, _proto, allTags, solution)) { conditionsMet = false; break; } } if (!conditionsMet) continue; selectedRecipe = recipe; break; } return selectedRecipe; } protected void CreateFoodData(Entity ent, CP14CookingRecipePrototype recipe) { if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out var soln, out var solution)) return; if (!_container.TryGetContainer(ent, ent.Comp.ContainerId, out var container)) return; var newData = new CP14FoodData(recipe.FoodData); //Assign recipe to the FoodData newData.CurrentRecipe = recipe.ID; //Process entities foreach (var contained in container.ContainedEntities) { if (TryComp(contained, out var food)) { //Merge trash newData.Trash.AddRange(food.Trash); //Merge solutions if (_solution.TryGetSolution(contained, food.Solution, out _, out var foodSolution)) { _solution.TryMixAndOverflow(soln.Value, foodSolution, solution.MaxVolume, out var overflowed); if (overflowed is not null) { _puddle.TrySplashSpillAt(ent, Transform(ent).Coordinates, overflowed, out _); } } } if (TryComp(contained, out var flavorComp)) { //Merge flavors foreach (var flavor in flavorComp.Flavors) { newData.Flavors.Add(flavor); } } QueueDel(contained); } if (solution.Volume <= 0) return; if (TryComp(ent.Owner, out var holder)) { holder.FoodData = newData; Dirty(ent.Owner, holder); } Dirty(ent); } protected void BurntFood(Entity ent) { if (!TryComp(ent, out var holder) || holder.FoodData is null) return; if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionId, out var soln, out var solution)) return; //Brown visual foreach (var visuals in holder.FoodData.Visuals) { visuals.Color = Color.FromHex("#212121"); } holder.FoodData.Name = Loc.GetString("cp14-meal-recipe-burned-trash-name"); holder.FoodData.Desc = Loc.GetString("cp14-meal-recipe-burned-trash-desc"); var replacedVolume = solution.Volume / 2; solution.SplitSolution(replacedVolume); solution.AddReagent(_burntFoodReagent, replacedVolume / 2); DirtyField(ent.Owner, holder, nameof(CP14FoodHolderComponent.FoodData)); } }