[Wiki] MealRecipes json generator (#1566)

This commit is contained in:
mhamster
2023-11-21 21:16:03 +07:00
committed by GitHub
parent 177ab521e5
commit 6702ec9832
10 changed files with 568 additions and 6 deletions

View File

@@ -79,3 +79,13 @@ jobs:
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload mealrecipes_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}

View File

@@ -1,4 +1,3 @@
using System.Linq;
using System.Text.Json.Serialization;
using Robust.Shared.Prototypes;

View File

@@ -1,11 +1,6 @@
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server.GuideGenerator;

View File

@@ -0,0 +1,83 @@
using System.Text.Json.Serialization;
using Robust.Shared.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Server.Kitchen.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
namespace Content.Server.GuideGenerator;
public sealed class GrindRecipeEntry
{
/// <summary>
/// Id of grindable item
/// </summary>
[JsonPropertyName("id")]
public string Id { get; }
/// <summary>
/// Human-readable name of recipe.
/// Should automatically be localized by default
/// </summary>
[JsonPropertyName("name")]
public string Name { get; }
/// <summary>
/// Type of recipe
/// </summary>
[JsonPropertyName("type")]
public string Type { get; }
/// <summary>
/// Item that will be grinded into something
/// </summary>
[JsonPropertyName("input")]
public string Input { get; }
/// <summary>
/// Dictionary of reagents that entity contains; aka "Recipe Result"
/// </summary>
[JsonPropertyName("result")]
public Dictionary<string, int> Result { get; } = new Dictionary<string, int>();
public GrindRecipeEntry(EntityPrototype proto)
{
Id = proto.ID;
if (proto.Name.Length > 1)
{
Name = char.ToUpper(proto.Name[0]) + proto.Name.Remove(0, 1);
}
else if (proto.Name.Length == 1)
{
Name = char.ToUpper(proto.Name[0]).ToString();
}
else
{
Name = proto.Name;
}
Type = "grindableRecipes";
Input = proto.ID;
var foodSulitonName = "food"; // default to food because everything in prototypes defaults to "food"
// Now, to become a recipe, entity must:
// A) Have "Extractable" component on it.
// B) Have "SolutionContainerManager" component on it.
// C) Have "GrindableSolution" declared in "SolutionContainerManager" component.
// D) Have solution with name declared in "SolutionContainerManager.GrindableSolution" inside its "SolutionContainerManager" component.
// F) Have "Food" in its name (see Content.Server/Corvax/GuideGenerator/MealsRecipesJsonGenerator.cs)
if (proto.Components.TryGetComponent("Extractable", out var extractableComp) && proto.Components.TryGetComponent("SolutionContainerManager", out var solutionCompRaw))
{
var extractable = (ExtractableComponent) extractableComp;
var solutionComp = (SolutionContainerManagerComponent) solutionCompRaw;
foodSulitonName = extractable.GrindableSolution;
if (foodSulitonName != null && solutionComp.Solutions.ContainsKey(foodSulitonName))
{
foreach (ReagentQuantity reagent in solutionComp.Solutions[(string) foodSulitonName].Contents)
{
Result[reagent.Reagent.Prototype] = reagent.Quantity.Int();
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Text.Json.Serialization;
using Robust.Shared.Prototypes;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Construction.Steps;
namespace Content.Server.GuideGenerator;
public sealed class HeatableRecipeEntry
{
/// <summary>
/// Id of recipe
/// </summary>
[JsonPropertyName("id")]
public string Id { get; }
/// <summary>
/// Human-readable name of recipe.
/// Should automatically be localized by default
/// </summary>
[JsonPropertyName("name")]
public string Name { get; }
/// <summary>
/// Type of recipe
/// </summary>
[JsonPropertyName("type")]
public string Type { get; }
/// <summary>
/// Temp, required for "input" thing to become "result" thing
/// </summary>
[JsonPropertyName("minTemp")]
public float MinTemp { get; }
/// <summary>
/// Item that will be transformed into something with enough temp
/// </summary>
[JsonPropertyName("input")]
public string Input { get; }
/// <summary>
/// Result of a recipe.
/// If it is null then recipe does not exist or we could not get recipe info.
/// </summary>
[JsonPropertyName("result")]
public string? Result { get; }
public HeatableRecipeEntry(
ConstructionGraphPrototype constructionProto, // to get data from construction prototype (minTemp, result)
EntityPrototype entityPrototype // to get entity data (name, input entity id)
)
{
var graphID = "";
var startNode = constructionProto.Nodes[constructionProto.Start!];
if (entityPrototype.Components.TryGetComponent("Construction", out var constructionCompRaw)) // does entity actually has Construction component?
{
foreach (var nodeEdgeRaw in startNode.Edges) // because we don't know what node contains heating step (in case if it is not constructionProto.Start) let's check every node and see if we will get anything
{
var nodeEdge = (ConstructionGraphEdge)nodeEdgeRaw;
foreach (var nodeStepRaw in nodeEdge.Steps)
{
if (nodeStepRaw.GetType().Equals(typeof(TemperatureConstructionGraphStep))) // TemperatureConstructionGraphStep is used only in steaks recipes, so for now we can afford it
{
var nodeStep = (TemperatureConstructionGraphStep)nodeStepRaw;
graphID = nodeEdge.Target; // required to check when we need to leave second loop; this is the best solution, because nodeEdge.Target is marked as required datafield and cannot be null
EntityManager em = new();
MinTemp = nodeStep.MinTemperature.HasValue ? nodeStep.MinTemperature.Value : 0;
Result = nodeStep.MinTemperature.HasValue ? constructionProto.Nodes[nodeEdge.Target].Entity.GetId(null, null, new GraphNodeEntityArgs(em)) : null;
break;
}
}
if (graphID != "") break; // we're done! let's leave!
}
if (graphID == "") // we've failed to get anything :(
{
MinTemp = 0;
Result = null;
}
}
else // if entity does not have construction component then it cannot be constructed - (c) Jason Statham
{
MinTemp = 0;
Result = null;
}
Input = entityPrototype.ID;
Name = entityPrototype.Name;
Id = entityPrototype.ID;
Type = "heatableRecipes";
}
}

View File

@@ -0,0 +1,127 @@
using System.IO;
using System.Text.RegularExpressions;
using System.Linq;
using System.Text.Json;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Kitchen;
using Robust.Shared.Prototypes;
using Content.Shared.Construction.Prototypes;
using Content.Server.Construction.Components;
using Content.Server.Chemistry.ReactionEffects;
namespace Content.Server.GuideGenerator;
public sealed class MicrowaveMealRecipeJsonGenerator
{
public static void PublishJson(StreamWriter file)
{
var prototype = IoCManager.Resolve<IPrototypeManager>();
var entities = prototype.EnumeratePrototypes<EntityPrototype>();
var constructable = prototype.EnumeratePrototypes<ConstructionGraphPrototype>();
var output = new Dictionary<string, dynamic>();
var microwaveRecipes =
prototype
.EnumeratePrototypes<FoodRecipePrototype>()
.Select(x => new MicrowaveRecipeEntry(x))
.ToDictionary(x => x.Id, x => x);
var sliceableRecipes =
entities
.Where(x => x.Components.TryGetComponent("SliceableFood", out var _))
.Select(x => new SliceRecipeEntry(x))
.Where(x => x.Result != "") // SOMEONE THOUGHT THAT IT WOULD BE A GREAT IDEA TO PUT COMPONENT ON AN ITEM WITHOUT SPECIFYING THE OUTPUT THING.
.Where(x => x.Count > 0) // Just in case.
.ToDictionary(x => x.Id, x => x);
var grindableRecipes =
entities
.Where(x => x.Components.TryGetComponent("Extractable", out var _))
.Where(x => x.Components.TryGetComponent("SolutionContainerManager", out var _))
.Where(x => (Regex.Match(x.ID.ToLower().Trim(), @".*[Ff]ood*").Success)) // we dont need some "organ" or "pills" prototypes.
.Select(x => new GrindRecipeEntry(x))
.ToDictionary(x => x.Id, x => x);
// construction-related items start
var constructionGraphs =
constructable
.Where(x => (Regex.Match(x.ID.ToLower().Trim(), @".*.*[Bb]acon*|.*[Ss]teak*|[Pp]izza*").Success)) // we only need recipes that has "bacon", "steak" and "pizza" in it, since they are the only "constructable" recipes
.ToDictionary(x => x.ID, x => x);
var constructableEntities = // list of entities which names match regex and has Construction component
entities
.Where(x => (Regex.Match(x.ID.ToLower().Trim(), @"(?<![Cc]rate)[Ff]ood*").Success))
.Where(x => x.Components.ContainsKey("Construction"))
.ToList();
var entityGraphs = new Dictionary<string, string>(); // BFH. Since we cannot get component from another .Where call (because of CS0103), let's keep everything in one temp dictionary.
foreach (var ent in constructableEntities)
{
if (ent.Components.TryGetComponent("Construction", out var constructionCompRaw))
{
var constructionComp = (ConstructionComponent)constructionCompRaw;
entityGraphs[ent.ID] = constructionComp.Graph;
}
}
var constructableHeatableEntities = constructableEntities // let's finally create our heatable recipes list
.Where(x => constructionGraphs.ContainsKey(entityGraphs[x.ID]))
.Select(x => new HeatableRecipeEntry(constructionGraphs[entityGraphs[x.ID]], x))
.Where(x => (x.Result != null))
.Where(x => x.Id != x.Result) // sometimes things dupe (for example if someone puts construction component on both inout and output things)
.ToDictionary(x => x.Id, x => x);
var constructableToolableEntities = constructableEntities // let's finally create our toolmade recipes list
.Where(x => constructionGraphs.ContainsKey(entityGraphs[x.ID]))
.Select(x => new ToolRecipeEntry(constructionGraphs[entityGraphs[x.ID]], x))
.Where(x => (x.Result != null))
.Where(x => x.Id != x.Result) // the same here, things sometimes dupe
.ToDictionary(x => x.Id, x => x);
// construction-related items end
// reaction-related items start
var reactionPrototypes =
prototype
.EnumeratePrototypes<ReactionPrototype>()
.Select(x => new ReactionEntry(x))
.ToList();
var mixableRecipes = new Dictionary<string, Dictionary<string, string>>(); // this is a list because we have https://station14.ru/wiki/Модуль:Chemistry_Lookup that already has everything we need and does everything for us.
foreach (var react in reactionPrototypes)
{
foreach (var effect in react.Effects)
if (effect.GetType().Equals(typeof(CreateEntityReactionEffect)))
{
var trueEffect = (CreateEntityReactionEffect)effect;
if (Regex.Match(trueEffect.Entity.ToLower().Trim(), @".*[Ff]ood*").Success) if (!mixableRecipes.ContainsKey(react.Id))
{
mixableRecipes[react.Id] = new Dictionary<string, string>();
mixableRecipes[react.Id]["id"] = react.Id;
mixableRecipes[react.Id]["type"] = "mixableRecipes";
}
}
}
// reaction-related items end
output["microwaveRecipes"] = microwaveRecipes;
output["sliceableRecipes"] = sliceableRecipes;
output["grindableRecipes"] = grindableRecipes;
output["heatableRecipes"] = constructableHeatableEntities;
output["toolmadeRecipes"] = constructableToolableEntities;
output["mixableRecipes"] = mixableRecipes;
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true
};
file.Write(JsonSerializer.Serialize(output, serializeOptions));
}
}

View File

@@ -0,0 +1,82 @@
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Kitchen;
namespace Content.Server.GuideGenerator;
public sealed class MicrowaveRecipeEntry
{
/// <summary>
/// Id of recipe
/// </summary>
[JsonPropertyName("id")]
public string Id { get; }
/// <summary>
/// Human-readable name of recipe.
/// Should automatically be localized by default
/// </summary>
[JsonPropertyName("name")]
public string Name { get; }
/// <summary>
/// Type of recipe
/// </summary>
[JsonPropertyName("type")]
public string Type { get; }
/// <summary>
/// Time to cook something (for microwave recipes)
/// </summary>
[JsonPropertyName("time")]
public uint Time { get; }
/// <summary>
/// Solids required to cook something
/// </summary>
[JsonPropertyName("solids")]
public Dictionary<string, uint> Solids { get; }
/// <summary>
/// Reagents required to cook something
/// </summary>
[JsonPropertyName("reagents")]
public Dictionary<string, uint> Reagents { get; }
/// <summary>
/// Result of a recipe
/// </summary>
[JsonPropertyName("result")]
public string Result { get; }
public MicrowaveRecipeEntry(FoodRecipePrototype proto)
{
Id = proto.ID;
if (proto.Name.Length > 1)
{
Name = char.ToUpper(proto.Name[0]) + proto.Name.Remove(0, 1);
}
else if (proto.Name.Length == 1)
{
Name = char.ToUpper(proto.Name[0]).ToString();
}
else
{
Name = proto.Name;
}
Type = "microwaveRecipes";
Time = proto.CookTime;
Solids = proto.IngredientsSolids
.ToDictionary(
sol => sol.Key,
sol => (uint)(int)sol.Value.Int()
);
Reagents = proto.IngredientsReagents
.ToDictionary(
rea => rea.Key,
rea => (uint)(int)rea.Value.Int()
);
Result = proto.Result;
}
}

View File

@@ -0,0 +1,76 @@
using System.Text.Json.Serialization;
using Robust.Shared.Prototypes;
using Content.Server.Nutrition.Components;
namespace Content.Server.GuideGenerator;
public sealed class SliceRecipeEntry
{
/// <summary>
/// Id of sliceable item
/// </summary>
[JsonPropertyName("id")]
public string Id { get; }
/// <summary>
/// Human-readable name of recipe.
/// Should automatically be localized by default
/// </summary>
[JsonPropertyName("name")]
public string Name { get; }
/// <summary>
/// Type of recipe
/// </summary>
[JsonPropertyName("type")]
public string Type { get; }
/// <summary>
/// Item that will be sliced into something
/// </summary>
[JsonPropertyName("input")]
public string Input { get; }
/// <summary>
/// Result of a recipe
/// </summary>
[JsonPropertyName("result")]
public string Result { get; }
/// <summary>
/// Count of result item
/// </summary>
[JsonPropertyName("count")]
public int Count { get; }
public SliceRecipeEntry(EntityPrototype proto)
{
Id = proto.ID;
if (proto.Name.Length > 1)
{
Name = char.ToUpper(proto.Name[0]) + proto.Name.Remove(0, 1);
}
else if (proto.Name.Length == 1)
{
Name = char.ToUpper(proto.Name[0]).ToString();
}
else
{
Name = proto.Name;
}
Type = "sliceableRecipes";
Input = proto.ID;
if (proto.Components.TryGetComponent("SliceableFood", out var comp))
{
var sliceable = (SliceableFoodComponent) comp;
Result = sliceable.Slice;
Count = sliceable.TotalCount;
}
else // just in case something will go wrong and we somehow will not get our component
{
Result = "";
Count = 0;
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Text.Json.Serialization;
using Robust.Shared.Prototypes;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Construction.Steps;
namespace Content.Server.GuideGenerator;
public sealed class ToolRecipeEntry // because of https://github.com/space-wizards/space-station-14/pull/20624, some recipes can now be cooked using tools
// actually, the code is pretty similar with HeatableRecipeEntry. The only difference is that we need ToolConstructionGraphStep instead of TemperatureConstructionGraphStep
// comments are left untouched :)
{
/// <summary>
/// Id of recipe
/// </summary>
[JsonPropertyName("id")]
public string Id { get; }
/// <summary>
/// Human-readable name of recipe.
/// Should automatically be localized by default
/// </summary>
[JsonPropertyName("name")]
public string Name { get; }
/// <summary>
/// Type of recipe
/// </summary>
[JsonPropertyName("type")]
public string Type { get; }
/// <summary>
/// Type of tool that is used to convert input into result
/// </summary>
[JsonPropertyName("tool")]
public string? Tool { get; }
/// <summary>
/// Item that will be transformed into something with enough temp
/// </summary>
[JsonPropertyName("input")]
public string Input { get; }
/// <summary>
/// Result of a recipe.
/// If it is null then recipe does not exist or we could not get recipe info.
/// </summary>
[JsonPropertyName("result")]
public string? Result { get; }
public ToolRecipeEntry(
ConstructionGraphPrototype constructionProto, // to get data from construction prototype (Tool, result)
EntityPrototype entityPrototype // to get entity data (name, input entity id)
)
{
var graphID = "";
var startNode = constructionProto.Nodes[constructionProto.Start!];
if (entityPrototype.Components.TryGetComponent("Construction", out var constructionCompRaw)) // does entity actually has Construction component?
{
foreach (var nodeEdgeRaw in startNode.Edges) // because we don't know what node contains heating step (in case if it is not constructionProto.Start) let's check every node and see if we will get anything
{
var nodeEdge = (ConstructionGraphEdge)nodeEdgeRaw;
foreach (var nodeStepRaw in nodeEdge.Steps)
{
if (nodeStepRaw.GetType().Equals(typeof(ToolConstructionGraphStep))) // ToolConstructionGraphStep is used only in steaks recipes, so for now we can afford it
{
var nodeStep = (ToolConstructionGraphStep)nodeStepRaw;
graphID = nodeEdge.Target; // required to check when we need to leave second loop; this is the best solution, because nodeEdge.Target is marked as required datafield and cannot be null
EntityManager em = new();
Tool = nodeStep.Tool;
Result = constructionProto.Nodes[nodeEdge.Target].Entity.GetId(null, null, new GraphNodeEntityArgs(em));
break;
}
}
if (graphID != "") break; // we're done! let's leave!
}
if (graphID == "") // we've failed to get anything :(
{
Tool = null;
Result = null;
}
}
else // if entity does not have construction component then it cannot be constructed - (c) Jason Statham
{
Tool = null;
Result = null;
}
Input = entityPrototype.ID;
Name = entityPrototype.Name;
Id = entityPrototype.ID;
Type = "toolmadeRecipes";
}
}

View File

@@ -136,6 +136,9 @@ namespace Content.Server.Entry
file = resourceManager.UserData.OpenWriteText(resPath.WithName("entity_" + dest));
EntityJsonGenerator.PublishJson(file);
file.Flush();
file = resourceManager.UserData.OpenWriteText(resPath.WithName("mealrecipes_" + dest));
MicrowaveMealRecipeJsonGenerator.PublishJson(file);
file.Flush();
// Corvax-Wiki-End
IoCManager.Resolve<IBaseServer>().Shutdown("Data generation done");
}