From 1dcb1c2fa516147dbdb0536654fa529eec284939 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:55:10 +0300 Subject: [PATCH 1/2] Food Recipe Guidebook (#783) # Description Adds an auto-generated list of recipes to the guidebook. This was mostly made using the chemical list as a reference, so it's a bit shitcode-ey. # TODO - [X] Make less ugly (add paddings to table cells and fix colors) - [X] Fix sprites not working - [X] Unshitcode (if possible)

Media

https://github.com/user-attachments/assets/597cbec1-7114-480b-ab8d-5ed9f8b2e0c3

# Changelog :cl: - add: The "food recipes" page in guidebook now contains an automatically generated list of food recipes. --- .../Controls/GuideFoodComposition.xaml | 21 +++ .../Controls/GuideFoodComposition.xaml.cs | 31 ++++ .../Guidebook/Controls/GuideFoodEmbed.xaml | 43 +++++ .../Guidebook/Controls/GuideFoodEmbed.xaml.cs | 161 ++++++++++++++++++ .../Controls/GuideFoodGroupEmbed.xaml | 4 + .../Controls/GuideFoodGroupEmbed.xaml.cs | 38 +++++ .../Guidebook/Controls/GuideFoodSource.xaml | 37 ++++ .../Controls/GuideFoodSource.xaml.cs | 160 +++++++++++++++++ .../EntitySystems/FoodGuideDataSystem.cs | 30 ++++ .../EntitySystems/FoodGuideDataSystem.cs | 138 +++++++++++++++ .../Components/ButcherableComponent.cs | 2 + .../SharedFoodGuideDataSystem.cs | 158 +++++++++++++++++ Resources/Locale/en-US/guidebook/food.ftl | 16 ++ .../Prototypes/Guidebook/shiftandcrew.yml | 1 + .../Guidebook/Service/FoodRecipes.xml | 87 +--------- 15 files changed, 844 insertions(+), 83 deletions(-) create mode 100644 Content.Client/Guidebook/Controls/GuideFoodComposition.xaml create mode 100644 Content.Client/Guidebook/Controls/GuideFoodComposition.xaml.cs create mode 100644 Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml create mode 100644 Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml.cs create mode 100644 Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml create mode 100644 Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml.cs create mode 100644 Content.Client/Guidebook/Controls/GuideFoodSource.xaml create mode 100644 Content.Client/Guidebook/Controls/GuideFoodSource.xaml.cs create mode 100644 Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs create mode 100644 Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SharedFoodGuideDataSystem.cs create mode 100644 Resources/Locale/en-US/guidebook/food.ftl diff --git a/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml b/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml new file mode 100644 index 00000000000..6a3072ce08b --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml.cs b/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml.cs new file mode 100644 index 00000000000..b4f25a02e4c --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodComposition.xaml.cs @@ -0,0 +1,31 @@ +using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideFoodComposition : BoxContainer, ISearchableControl +{ + public GuideFoodComposition(ReagentPrototype proto, FixedPoint2 quantity) + { + RobustXamlLoader.Load(this); + + ReagentLabel.Text = proto.LocalizedName; + AmountLabel.Text = quantity.ToString(); + } + + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml b/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml new file mode 100644 index 00000000000..8aa2b10356e --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml.cs new file mode 100644 index 00000000000..355d3a020f8 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodEmbed.xaml.cs @@ -0,0 +1,161 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Client.Chemistry.EntitySystems; +using Content.Client.Guidebook.Richtext; +using Content.Client.Message; +using Content.Client.Nutrition.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Guidebook.Controls; + +/// +/// Control for embedding a food recipe into a guidebook. +/// +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideFoodEmbed : BoxContainer, IDocumentTag, ISearchableControl +{ + [Dependency] private readonly IEntitySystemManager _systemManager = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private readonly FoodGuideDataSystem _foodGuideData; + private readonly ISawmill _logger = default!; + + public GuideFoodEmbed() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _foodGuideData = _systemManager.GetEntitySystem(); + _logger = Logger.GetSawmill("food guide"); + MouseFilter = MouseFilterMode.Stop; + } + + public GuideFoodEmbed(FoodGuideEntry entry) : this() + { + GenerateControl(entry); + } + + public bool CheckMatchesSearch(string query) + { + return FoodName.GetMessage()?.Contains(query, StringComparison.InvariantCultureIgnoreCase) == true + || FoodDescription.GetMessage()?.Contains(query, StringComparison.InvariantCultureIgnoreCase) == true; + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } + + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + control = null; + if (!args.TryGetValue("Food", out var id)) + { + _logger.Error("Food embed tag is missing food prototype argument."); + return false; + } + + if (!_foodGuideData.TryGetData(id, out var data)) + { + _logger.Warning($"Specified food prototype \"{id}\" does not have any known sources."); + return false; + } + + GenerateControl(data); + + control = this; + return true; + } + + private void GenerateControl(FoodGuideEntry data) + { + _prototype.TryIndex(data.Result, out var proto); + if (proto == null) + { + FoodName.SetMarkup(Loc.GetString("guidebook-food-unknown-proto", ("id", data.Result))); + return; + } + + var composition = data.Composition + .Select(it => _prototype.TryIndex(it.Reagent.Prototype, out var reagent) ? (reagent, it.Quantity) : (null, 0)) + .Where(it => it.reagent is not null) + .Cast<(ReagentPrototype, FixedPoint2)>() + .ToList(); + + #region Colors + + CalculateColors(composition, out var textColor, out var backgroundColor); + + NameBackground.PanelOverride = new StyleBoxFlat + { + BackgroundColor = backgroundColor + }; + FoodName.SetMarkup(Loc.GetString("guidebook-food-name", ("color", textColor), ("name", proto.Name))); + + #endregion + + #region Sources + + foreach (var source in data.Sources.OrderBy(it => it.OutputCount)) + { + var control = new GuideFoodSource(proto, source, _prototype); + SourcesDescriptionContainer.AddChild(control); + } + + #endregion + + #region Composition + + foreach (var (reagent, quantity) in composition) + { + var control = new GuideFoodComposition(reagent, quantity); + CompositionDescriptionContainer.AddChild(control); + } + + #endregion + + FormattedMessage description = new(); + description.AddText(proto?.Description ?? string.Empty); + // Cannot describe food flavor or smth beause food is entirely server-side + + FoodDescription.SetMessage(description); + } + + private void CalculateColors(List<(ReagentPrototype, FixedPoint2)> composition, out Color text, out Color background) + { + // Background color is calculated as the weighted average of the colors of the composition. + // Text color is determined based on background luminosity. + float r = 0, g = 0, b = 0; + FixedPoint2 weight = 0; + + foreach (var (proto, quantity) in composition) + { + var tcolor = proto.SubstanceColor; + var prevalence = + quantity <= 0 ? 0f + : weight == 0f ? 1f + : (quantity / (weight + quantity)).Float(); + + r = r * (1 - prevalence) + tcolor.R * prevalence; + g = g * (1 - prevalence) + tcolor.G * prevalence; + b = b * (1 - prevalence) + tcolor.B * prevalence; + + if (quantity > 0) + weight += quantity; + } + + // Copied from GuideReagentEmbed which was probably copied from stackoverflow. This is the formula for color luminosity. + var lum = 0.2126f * r + 0.7152f * g + 0.0722f; + + background = new Color(r, g, b); + text = lum > 0.5f ? Color.Black : Color.White; + } +} diff --git a/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml b/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml new file mode 100644 index 00000000000..da671adaa72 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml @@ -0,0 +1,4 @@ + + + diff --git a/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml.cs new file mode 100644 index 00000000000..0e1034e3943 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodGroupEmbed.xaml.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Client.Guidebook.Richtext; +using Content.Client.Nutrition.EntitySystems; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideFoodGroupEmbed : BoxContainer, IDocumentTag +{ + [Dependency] private readonly IEntitySystemManager _sysMan = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public GuideFoodGroupEmbed() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + MouseFilter = MouseFilterMode.Stop; + + foreach (var data in _sysMan.GetEntitySystem().Registry.OrderBy(it => it.Identifier)) + { + var embed = new GuideFoodEmbed(data); + GroupContainer.AddChild(embed); + } + } + + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + control = this; + return true; + } +} diff --git a/Content.Client/Guidebook/Controls/GuideFoodSource.xaml b/Content.Client/Guidebook/Controls/GuideFoodSource.xaml new file mode 100644 index 00000000000..74e3a2ec30d --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodSource.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/Content.Client/Guidebook/Controls/GuideFoodSource.xaml.cs b/Content.Client/Guidebook/Controls/GuideFoodSource.xaml.cs new file mode 100644 index 00000000000..b0fee709759 --- /dev/null +++ b/Content.Client/Guidebook/Controls/GuideFoodSource.xaml.cs @@ -0,0 +1,160 @@ +using System.Linq; +using Content.Client.Chemistry.EntitySystems; +using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.Components; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideFoodSource : BoxContainer, ISearchableControl +{ + private readonly IPrototypeManager _protoMan; + private readonly SpriteSystem _sprites = default!; + + public GuideFoodSource(IPrototypeManager protoMan) + { + RobustXamlLoader.Load(this); + _protoMan = protoMan; + _sprites = IoCManager.Resolve().GetEntitySystem(); + } + + public GuideFoodSource(EntityPrototype result, FoodSourceData entry, IPrototypeManager protoMan) : this(protoMan) + { + switch (entry) + { + case FoodButcheringData butchering: + GenerateControl(butchering); + break; + case FoodSlicingData slicing: + GenerateControl(slicing); + break; + case FoodRecipeData recipe: + GenerateControl(recipe); + break; + case FoodReactionData reaction: + GenerateControl(reaction); + break; + default: + throw new ArgumentOutOfRangeException(nameof(entry), entry, null); + } + + GenerateOutputs(result, entry); + } + + private void GenerateControl(FoodButcheringData entry) + { + if (!_protoMan.TryIndex(entry.Butchered, out var ent)) + { + SourceLabel.SetMessage(Loc.GetString("guidebook-food-unknown-proto", ("id", entry.Butchered))); + return; + } + + SetSource(ent); + ProcessingLabel.Text = Loc.GetString("guidebook-food-processing-butchering"); + + ProcessingTexture.Texture = entry.Type switch + { + ButcheringType.Knife => GetRsiTexture("/Textures/Objects/Weapons/Melee/kitchen_knife.rsi", "icon"), + _ => GetRsiTexture("/Textures/Structures/meat_spike.rsi", "spike") + }; + } + + private void GenerateControl(FoodSlicingData entry) + { + if (!_protoMan.TryIndex(entry.Sliced, out var ent)) + { + SourceLabel.SetMessage(Loc.GetString("guidebook-food-unknown-proto", ("id", entry.Sliced))); + return; + } + + SetSource(ent); + ProcessingLabel.Text = Loc.GetString("guidebook-food-processing-slicing"); + ProcessingTexture.Texture = GetRsiTexture("/Textures/Objects/Misc/utensils.rsi", "plastic_knife"); + } + + private void GenerateControl(FoodRecipeData entry) + { + if (!_protoMan.TryIndex(entry.Recipe, out var recipe)) + { + SourceLabel.SetMessage(Loc.GetString("guidebook-food-unknown-proto", ("id", entry.Result))); + return; + } + + var combinedSolids = recipe.IngredientsSolids + .Select(it => _protoMan.TryIndex(it.Key, out var proto) ? FormatIngredient(proto, it.Value) : "") + .Where(it => it.Length > 0); + var combinedLiquids = recipe.IngredientsReagents + .Select(it => _protoMan.TryIndex(it.Key, out var proto) ? FormatIngredient(proto, it.Value) : "") + .Where(it => it.Length > 0); + + var combinedIngredients = string.Join("\n", combinedLiquids.Union(combinedSolids)); + SourceLabel.SetMessage(Loc.GetString("guidebook-food-processing-recipe", ("ingredients", combinedIngredients))); + + ProcessingTexture.Texture = GetRsiTexture("/Textures/Structures/Machines/microwave.rsi", "mw"); + ProcessingLabel.Text = Loc.GetString("guidebook-food-processing-cooking", ("time", recipe.CookTime)); + } + + private void GenerateControl(FoodReactionData entry) + { + if (!_protoMan.TryIndex(entry.Reaction, out var reaction)) + { + SourceLabel.SetMessage(Loc.GetString("guidebook-food-unknown-proto", ("id", entry.Reaction))); + return; + } + + var combinedReagents = reaction.Reactants + .Select(it => _protoMan.TryIndex(it.Key, out var proto) ? FormatIngredient(proto, it.Value.Amount) : "") + .Where(it => it.Length > 0); + + SourceLabel.SetMessage(Loc.GetString("guidebook-food-processing-recipe", ("ingredients", string.Join("\n", combinedReagents)))); + ProcessingTexture.TexturePath = "/Textures/Interface/Misc/beakerlarge.png"; + ProcessingLabel.Text = Loc.GetString("guidebook-food-processing-reaction"); + } + + private Texture GetRsiTexture(string path, string state) + { + return _sprites.Frame0(new SpriteSpecifier.Rsi(new ResPath(path), state)); + } + + private void GenerateOutputs(EntityPrototype result, FoodSourceData entry) + { + OutputsLabel.Text = Loc.GetString("guidebook-food-output", ("name", result.Name), ("number", entry.OutputCount)); + OutputsTexture.Texture = _sprites.Frame0(result); + } + + private void SetSource(EntityPrototype ent) + { + SourceLabel.SetMessage(ent.Name); + OutputsTexture.Texture = _sprites.Frame0(ent); + } + + private string FormatIngredient(EntityPrototype proto, FixedPoint2 amount) + { + return Loc.GetString("guidebook-food-ingredient-solid", ("name", proto.Name), ("amount", amount)); + } + + private string FormatIngredient(ReagentPrototype proto, FixedPoint2 amount) + { + return Loc.GetString("guidebook-food-ingredient-liquid", ("name", proto.LocalizedName), ("amount", amount)); + } + + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs b/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs new file mode 100644 index 00000000000..37c7a25e219 --- /dev/null +++ b/Content.Client/Nutrition/EntitySystems/FoodGuideDataSystem.cs @@ -0,0 +1,30 @@ +using Content.Client.Chemistry.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Client.Nutrition.EntitySystems; + +public sealed class FoodGuideDataSystem : SharedFoodGuideDataSystem +{ + public override void Initialize() + { + SubscribeNetworkEvent(OnReceiveRegistryUpdate); + } + + private void OnReceiveRegistryUpdate(FoodGuideRegistryChangedEvent message) + { + Registry = message.Changeset; + } + + public bool TryGetData(EntProtoId result, out FoodGuideEntry entry) + { + var index = Registry.FindIndex(it => it.Result == result); + if (index == -1) + { + entry = default; + return false; + } + + entry = Registry[index]; + return true; + } +} diff --git a/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs new file mode 100644 index 00000000000..f21c509ace2 --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/FoodGuideDataSystem.cs @@ -0,0 +1,138 @@ +using System.Linq; +using Content.Client.Chemistry.EntitySystems; +using Content.Server.Chemistry.ReactionEffects; +using Content.Server.Nutrition.Components; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Kitchen; +using Content.Shared.Nutrition.Components; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.Nutrition.EntitySystems; + +public sealed class FoodGuideDataSystem : SharedFoodGuideDataSystem +{ + public static readonly ProtoId[] ReagentWhitelist = + [ + "Nutriment", + "Vitamin", + "Protein", + "UncookedAnimalProteins", + "Fat", + "Water" + ]; + + public static readonly string[] ComponentNamesBlacklist = ["HumanoidAppearance"]; + + public static readonly string[] SuffixBlacklist = ["debug", "do not map", "admeme"]; + + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + + private Dictionary> _sources = new(); + + public override void Initialize() + { + SubscribeLocalEvent(OnPrototypesReloaded); + _player.PlayerStatusChanged += OnPlayerStatusChanged; + + ReloadRecipes(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified() + && !args.WasModified() + && !args.WasModified() + ) + return; + + ReloadRecipes(); + } + + public void ReloadRecipes() + { + // TODO: add this code to the list of known recipes because this is spaghetti + _sources.Clear(); + + // Butcherable and slicable entities + foreach (var ent in _protoMan.EnumeratePrototypes()) + { + if (ent.Abstract + || ent.Components.Any(it => ComponentNamesBlacklist.Contains(it.Key)) + || ent.SetSuffix is {} suffix && SuffixBlacklist.Any(it => suffix.Contains(it, StringComparison.OrdinalIgnoreCase)) + ) + continue; + + if (ent.TryGetComponent(out var butcherable)) + { + var butcheringSource = new FoodButcheringData(ent, butcherable); + foreach (var butchlet in butcherable.SpawnedEntities) + { + if (butchlet.PrototypeId is null) + continue; + + _sources.GetOrNew(butchlet.PrototypeId).Add(butcheringSource); + } + } + + if (ent.TryGetComponent(out var slicable) && slicable.Slice is not null) + { + _sources.GetOrNew(slicable.Slice).Add(new FoodSlicingData(ent, slicable.Slice.Value, slicable.TotalCount)); + } + } + + // Recipes + foreach (var recipe in _protoMan.EnumeratePrototypes()) + { + _sources.GetOrNew(recipe.Result).Add(new FoodRecipeData(recipe)); + } + + // Entity-spawning reactions + foreach (var reaction in _protoMan.EnumeratePrototypes()) + { + foreach (var effect in reaction.Effects) + { + if (effect is not CreateEntityReactionEffect entEffect) + continue; + + _sources.GetOrNew(entEffect.Entity).Add(new FoodReactionData(reaction, entEffect.Entity, (int) entEffect.Number)); + } + } + + Registry.Clear(); + + foreach (var (result, sources) in _sources) + { + var proto = _protoMan.Index(result); + var composition = proto.TryGetComponent(out var food) && proto.TryGetComponent(out var manager) + ? manager?.Solutions?[food.Solution]?.Contents?.ToArray() ?? [] + : []; + + // We filter out food without whitelisted reagents because well when people look for food they usually expect FOOD and not insulated gloves. + // And we get insulated and other gloves because they have ButcherableComponent and they are also moth food + if (!composition.Any(it => ReagentWhitelist.Contains(it.Reagent.Prototype))) + continue; + + // We also limit the number of sources to 10 because it's a huge performance strain to render 500 raw meat recipes. + var distinctSources = sources.DistinctBy(it => it.Identitier).Take(10); + var entry = new FoodGuideEntry(result, proto.Name, distinctSources.ToArray(), composition); + Registry.Add(entry); + } + + RaiseNetworkEvent(new FoodGuideRegistryChangedEvent(Registry)); + } + + private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs args) + { + if (args.NewStatus != SessionStatus.Connected) + return; + + RaiseNetworkEvent(new FoodGuideRegistryChangedEvent(Registry), args.Session); + } +} diff --git a/Content.Shared/Nutrition/Components/ButcherableComponent.cs b/Content.Shared/Nutrition/Components/ButcherableComponent.cs index 4fce45422ad..975d4329dc5 100644 --- a/Content.Shared/Nutrition/Components/ButcherableComponent.cs +++ b/Content.Shared/Nutrition/Components/ButcherableComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Storage; using Robust.Shared.GameStates; +using Robust.Shared.Serialization; namespace Content.Shared.Nutrition.Components { @@ -25,6 +26,7 @@ public sealed partial class ButcherableComponent : Component public bool BeingButchered; } + [Serializable, NetSerializable] public enum ButcheringType : byte { Knife, // e.g. goliaths diff --git a/Content.Shared/Nutrition/EntitySystems/SharedFoodGuideDataSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedFoodGuideDataSystem.cs new file mode 100644 index 00000000000..c31776cd756 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SharedFoodGuideDataSystem.cs @@ -0,0 +1,158 @@ +using System.Linq; +using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Kitchen; +using Content.Shared.Nutrition.Components; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Client.Chemistry.EntitySystems; + +public abstract class SharedFoodGuideDataSystem : EntitySystem +{ + public List Registry = new(); +} + +[Serializable, NetSerializable] +public sealed class FoodGuideRegistryChangedEvent : EntityEventArgs +{ + [DataField] + public List Changeset; + + public FoodGuideRegistryChangedEvent(List changeset) + { + Changeset = changeset; + } +} + +[DataDefinition, Serializable, NetSerializable] +public partial struct FoodGuideEntry +{ + [DataField] + public EntProtoId Result; + + [DataField] + public string Identifier; // Used for sorting + + [DataField] + public FoodSourceData[] Sources; + + [DataField] + public ReagentQuantity[] Composition; + + public FoodGuideEntry(EntProtoId result, string identifier, FoodSourceData[] sources, ReagentQuantity[] composition) + { + Result = result; + Identifier = identifier; + Sources = sources; + Composition = composition; + } +} + +[ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] +public abstract partial class FoodSourceData +{ + /// + /// Number of products created from this source. Used for primary ordering. + /// + public abstract int OutputCount { get; } + + /// + /// A string used to distinguish different sources. Typically the name of the related entity. + /// + public string Identitier; + + public abstract bool IsSourceOf(EntProtoId food); +} + +[Serializable, NetSerializable] +public sealed partial class FoodButcheringData : FoodSourceData +{ + [DataField] + public EntProtoId Butchered; + + [DataField] + public ButcheringType Type; + + [DataField] + public List Results; + + public override int OutputCount => Results.Count; + + public FoodButcheringData(EntityPrototype butchered, ButcherableComponent comp) + { + Identitier = butchered.Name; + Butchered = butchered.ID; + Type = comp.Type; + Results = comp.SpawnedEntities; + } + + public override bool IsSourceOf(EntProtoId food) => Results.Any(it => it.PrototypeId == food); +} + +[Serializable, NetSerializable] +public sealed partial class FoodSlicingData : FoodSourceData +{ + [DataField] + public EntProtoId Sliced, Result; + + [DataField] + private int _outputCount; + public override int OutputCount => _outputCount; + + public FoodSlicingData(EntityPrototype sliced, EntProtoId result, int outputCount) + { + Identitier = sliced.Name; + Sliced = sliced.ID; + Result = result; + _outputCount = outputCount; // Server-only + } + + public override bool IsSourceOf(EntProtoId food) => food == Result; +} + +[Serializable, NetSerializable] +public sealed partial class FoodRecipeData : FoodSourceData +{ + [DataField] + public ProtoId Recipe; + + [DataField] + public EntProtoId Result; + + public override int OutputCount => 1; + + public FoodRecipeData(FoodRecipePrototype proto) + { + Identitier = proto.Name; + Recipe = proto.ID; + Result = proto.Result; + } + + public override bool IsSourceOf(EntProtoId food) => food == Result; +} + +[Serializable, NetSerializable] +public sealed partial class FoodReactionData : FoodSourceData +{ + [DataField] + public ProtoId Reaction; + + [DataField] + public EntProtoId Result; + + [DataField] + private int _outputCount; + public override int OutputCount => _outputCount; + + public FoodReactionData(ReactionPrototype reaction, EntProtoId result, int outputCount) + { + Identitier = reaction.Name; + Reaction = reaction.ID; + Result = result; + _outputCount = outputCount; + } + + public override bool IsSourceOf(EntProtoId food) => food == Result; +} diff --git a/Resources/Locale/en-US/guidebook/food.ftl b/Resources/Locale/en-US/guidebook/food.ftl new file mode 100644 index 00000000000..bd4fa032516 --- /dev/null +++ b/Resources/Locale/en-US/guidebook/food.ftl @@ -0,0 +1,16 @@ +guidebook-food-name = [bold][color={$color}]{CAPITALIZE($name)}[/color][/bold] +guidebook-food-unknown-proto = Unknown prototype +guidebook-food-sources-header = Sources +guidebook-food-sources-ent-wrapper = {$name} +guidebook-food-reagents-header = Chemical composition + +guidebook-food-processing-butchering = Butcher +guidebook-food-processing-slicing = Slice +guidebook-food-processing-cooking = Microwave for {$time}s +guidebook-food-processing-reaction = Mix + +guidebook-food-processing-recipe = {$ingredients} +guidebook-food-ingredient-solid = add {$amount} {$name} +guidebook-food-ingredient-liquid = add {$amount}u {$name} + +guidebook-food-output = {$name} ({$number}) diff --git a/Resources/Prototypes/Guidebook/shiftandcrew.yml b/Resources/Prototypes/Guidebook/shiftandcrew.yml index 3c4618902e6..66f9e7316dc 100644 --- a/Resources/Prototypes/Guidebook/shiftandcrew.yml +++ b/Resources/Prototypes/Guidebook/shiftandcrew.yml @@ -41,3 +41,4 @@ id: Food Recipes name: guide-entry-foodrecipes text: "/ServerInfo/Guidebook/Service/FoodRecipes.xml" + filterEnabled: true diff --git a/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml b/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml index 797591dd783..c74b947dbf6 100644 --- a/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml +++ b/Resources/ServerInfo/Guidebook/Service/FoodRecipes.xml @@ -1,88 +1,9 @@ -## Starting Out -This is not an extensive list of recipes, these listings are to showcase the basics. +## Recipe list +Note: Only solid foods are listed here! To learn recipes for liquid ingredients, check the chemistry guidebook. -Mixes are done in a Beaker, foods are cooked in a Microwave. Cook times will be listed. +This list is auto-generated and contains all known foods. -WARNING: This is not an automatically generated list, things here may become outdated. The wiki has much more than is listed here. - -## The Basics: Mixing - -- Dough = 15 Flour, 10 Water -- Cornmeal Dough = 1 Egg (6u), 10 Milk, 15 Cornmeal -- Tortila Dough = 15 Cornmeal, 10 Water -- Tofu = 5 Enzyme (Catalyst), 30 Soy Milk -- Pie Dough = 2 Eggs (12u), 15 Flour, 5 Table Salt -- Cake Batter = 2 Eggs(12u), 15 flour, 5 Sugar -- Vegan Cake Batter = 15 Soy Milk, 15 Flour, 5 Sugar -- Butter = 30 Milk, 5 Table Salt (Catalyst) -- Cheese Wheel = 5 Enzyme (Catalyst), 40 Milk -- Chèvre Log = 5 Enzyme (Catalyst), 10 Goat Milk -- Meatball = 1 Egg (6u), 5 Flour, 5 Uncooked Animal Proteins -- Chocolate = 6 Cocoa Powder, 2 Milk, 2 Sugar -- Uncooked Animal Protein: Grind Raw Meat - - - - - - - - - - - - - - - - - - - -## Secondary Products - -- Dough Slice: Cut Dough -- Bun: Microwave Dough Slice for 5 Seconds -- Cutlet: Slice Raw Meat -- Cheese Wedge: Slice Cheese Wheel -- Flat Dough: Use a rolling pin or a round object (fire extinguisher, soda can, bottle) on Dough. -- Tortilla Dough Slice: cut Tortilla Dough -- Flat Tortilla Dough: Use a rolling pin or a round object (fire extinguisher, soda can, bottle) on Tortilla Dough Slice -- Taco Shell: Microwave Flat Tortilla Dough for 5 Seconds - -## Food Examples - -- Bread: Microwave Dough for 10 Seconds -- Plain Burger: Microwave 1 Bun and 1 Raw Meat for 10 Seconds -- Tomato Soup: 10u Water, 1 Bowl, and 2 Tomatoes for 10 Seconds -- Citrus Salad: 1 Bowl, 1 Lemon, 1 Lime, 1 Orange for 5 Seconds -- Margherita Pizza: Microwave 1 Flat Dough, 1 Cheese Wedge, and 4 Tomatoes for 30 Seconds -- Cake: 1 Cake Batter for 15 Seconds -- Apple Pie: 1 Pie Dough, 3 Apples, and 1 Pie Tin for 15 Seconds -- Beef Taco: Microwave 1 Taco Shell, 1 Raw Meat Cutlet, 1 Cheese Wedge for 10 Seconds -- Cuban Carp : Microwave 1 Dough, 1 Cheese Wedge, 1 Chili, 1 Carp Meat for 15 Seconds -- Banana Cream Pie : Microwave 1 Pie Dough, 3 Bananas, and 1 Pie Tin for 15 Seconds -- Carrot Fries : Microwave 1 Carrot, 15u Salt for 15 Seconds -- Pancake : Microwave 5u Flour, 5u Milk, 1 Egg (6u) for 5 Seconds - - - - - - - - - - - - - - - - - - - + From 2384e9c868d154c5280caccacad7610e03938522 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Wed, 11 Sep 2024 17:55:36 +0000 Subject: [PATCH 2/2] Automatic Changelog Update (#783) --- Resources/Changelog/Changelog.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d7068562a8e..5460e18f650 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6130,3 +6130,12 @@ Entries: id: 6340 time: '2024-09-09T14:21:56.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/794 +- author: Mnemotechnician + changes: + - type: Add + message: >- + The "food recipes" page in guidebook now contains an automatically + generated list of food recipes. + id: 6341 + time: '2024-09-11T17:55:11.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/783