diff --git a/.github/workflows/update-wiki.yml b/.github/workflows/update-wiki.yml new file mode 100644 index 00000000000..958f0afcd21 --- /dev/null +++ b/.github/workflows/update-wiki.yml @@ -0,0 +1,92 @@ +name: Update Wiki + +on: + workflow_dispatch: + push: + branches: [ master, jsondump ] + paths: + - '.github/workflows/update-wiki.yml' + - 'Content.Shared/Chemistry/**.cs' + - 'Content.Server/Chemistry/**.cs' + - 'Content.Server/GuideGenerator/**.cs' + - 'Content.Server/Corvax/GuideGenerator/**.cs' + - 'Resources/Prototypes/Reagents/**.yml' + - 'Resources/Prototypes/Chemistry/**.yml' + - 'Resources/Prototypes/Recipes/Reactions/**.yml' + - 'RobustToolbox/' + +jobs: + update-wiki: + name: Build and Publish JSON blobs to wiki + runs-on: ubuntu-latest + + steps: + - name: Checkout Master + uses: actions/checkout@v3.6.0 + + - name: Setup Submodule + run: | + git submodule update --init --recursive + + - name: Pull Engine Updates + uses: space-wizards/submodule-dependency@v0.1.5 + + - name: Update Engine Submodules + run: | + cd RobustToolbox/ + git submodule update --init --recursive + + - name: Setup .NET Core + uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: 7.0.x + + - name: Install Dependencies + run: dotnet restore + + - name: Build Project + run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors=nullable /m + + - name: Generate JSON blobs for prototypes + run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json + continue-on-error: true + + - name: Upload chem_prototypes.json to wiki + uses: jtmullen/mediawiki-edit-action@v0.1.1 + with: + wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json + edit_summary: Update chem_prototypes.json via GitHub Actions + page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json" + api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php + username: ${{ secrets.WIKI_BOT_USER }} + password: ${{ secrets.WIKI_BOT_PASS }} + + - name: Upload react_prototypes.json to wiki + uses: jtmullen/mediawiki-edit-action@v0.1.1 + with: + wiki_text_file: ./bin/Content.Server/data/react_prototypes.json + edit_summary: Update react_prototypes.json via GitHub Actions + page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json" + api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php + username: ${{ secrets.WIKI_BOT_USER }} + password: ${{ secrets.WIKI_BOT_PASS }} + + - name: Upload entity_prototypes.json to wiki + uses: jtmullen/mediawiki-edit-action@v0.1.1 + with: + wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json + edit_summary: Update entity_prototypes.json via GitHub Actions + page_name: "${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json" + 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 }} diff --git a/Content.Server/Corvax/GuideGenerator/EntityEntry.cs b/Content.Server/Corvax/GuideGenerator/EntityEntry.cs new file mode 100644 index 00000000000..12f71a59ead --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/EntityEntry.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using Robust.Shared.Prototypes; + +namespace Content.Server.GuideGenerator; + +public sealed class EntityEntry +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("desc")] + public string Description { get; } + + public EntityEntry(EntityPrototype proto) + { + Id = proto.ID; + Name = TextTools.TextTools.CapitalizeString(proto.Name); // Corvax-Wiki + Description = proto.Description; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs new file mode 100644 index 00000000000..83669c23cd8 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Linq; +using System.Text.Json; +using Robust.Shared.Prototypes; + +namespace Content.Server.GuideGenerator; + +public sealed class EntityJsonGenerator +{ + public static void PublishJson(StreamWriter file) + { + var prototype = IoCManager.Resolve(); + var prototypes = + prototype + .EnumeratePrototypes() + .Where(x => !x.Abstract) + .Select(x => new EntityEntry(x)) + .ToDictionary(x => x.Id, x => x); + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + }; + + file.Write(JsonSerializer.Serialize(prototypes, serializeOptions)); + } +} diff --git a/Content.Server/Corvax/GuideGenerator/GrindRecipeEntry.cs b/Content.Server/Corvax/GuideGenerator/GrindRecipeEntry.cs new file mode 100644 index 00000000000..989e78e4d19 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/GrindRecipeEntry.cs @@ -0,0 +1,74 @@ +using System.Text.Json.Serialization; +using Robust.Shared.Prototypes; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Kitchen.Components; + +namespace Content.Server.GuideGenerator; + +public sealed class GrindRecipeEntry +{ + /// + /// Id of grindable item + /// + [JsonPropertyName("id")] + public string Id { get; } + + /// + /// Human-readable name of recipe. + /// Should automatically be localized by default + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Type of recipe + /// + [JsonPropertyName("type")] + public string Type { get; } + + /// + /// Item that will be grinded into something + /// + [JsonPropertyName("input")] + public string Input { get; } + + /// + /// Dictionary of reagents that entity contains; aka "Recipe Result" + /// + [JsonPropertyName("result")] + public Dictionary? Result { get; } = new Dictionary(); + + + public GrindRecipeEntry(EntityPrototype proto) + { + Id = proto.ID; + Name = TextTools.TextTools.CapitalizeString(proto.Name); + Type = "grindableRecipes"; + Input = proto.ID; + var foodSolutionName = "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; + foodSolutionName = extractable.GrindableSolution; + + if (solutionComp.Solutions != null && foodSolutionName != null) + { + foreach (ReagentQuantity reagent in solutionComp.Solutions[(string) foodSolutionName].Contents) + { + Result[reagent.Reagent.Prototype] = reagent.Quantity.Int(); + } + } + else + Result = null; + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/HealthChangeReagentsJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/HealthChangeReagentsJsonGenerator.cs new file mode 100644 index 00000000000..2418d54a338 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/HealthChangeReagentsJsonGenerator.cs @@ -0,0 +1,92 @@ +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; +using System.IO; +using System.Linq; +using System.Text.Json; +using Content.Server.Chemistry.ReagentEffects; + +namespace Content.Server.Corvax.GuideGenerator; +public sealed class HealthChangeReagentsJsonGenerator +{ + public static void PublishJson(StreamWriter file) + { + var prototype = IoCManager.Resolve(); + + Dictionary>> healthChangeReagents = new(); + + // Сбор данных + foreach (var reagent in prototype.EnumeratePrototypes()) + { + if (reagent.Metabolisms is null) continue; + + foreach (var metabolism in reagent.Metabolisms) + { + foreach (HealthChange effect in metabolism.Value.Effects.Where(x => x is HealthChange)) + { + foreach (var damage in effect.Damage.DamageDict) + { + var damageType = damage.Key; + var damageChangeType = damage.Value.Float() < 0 ? "health" : "damage"; + + if (!healthChangeReagents.ContainsKey(damageType)) + { + healthChangeReagents.Add(damageType, new()); + } + + if (!healthChangeReagents[damageType].ContainsKey(damageChangeType)) + { + healthChangeReagents[damageType].Add(damageChangeType, new()); + } + + // Берем максимальный показатель (один реагент может наносить разный урон при разных условиях) + var damageChangeValueAbs = Math.Abs(damage.Value.Float() / metabolism.Value.MetabolismRate.Float()); // вычисляем показатель за 1 ед. вещества, а не 1 сек. нахождения я в организме. + if (healthChangeReagents[damageType][damageChangeType].TryGetValue(reagent.ID, out var previousValue)) + { + healthChangeReagents[damageType][damageChangeType][reagent.ID] = Math.Max(previousValue, damageChangeValueAbs); + } + else healthChangeReagents[damageType][damageChangeType].Add(reagent.ID, damageChangeValueAbs); + } + } + } + } + + // Сортировка + Dictionary>> healthChangeReagentsSorted = new(); + + foreach (var damageType in healthChangeReagents) + { + foreach (var damageChangeType in damageType.Value) + { + foreach (var reagent in damageChangeType.Value) + { + if (!healthChangeReagentsSorted.ContainsKey(damageType.Key)) + { + healthChangeReagentsSorted.Add(damageType.Key, new()); + } + + if (!healthChangeReagentsSorted[damageType.Key].ContainsKey(damageChangeType.Key)) + { + healthChangeReagentsSorted[damageType.Key].Add(damageChangeType.Key, new()); + } + + healthChangeReagentsSorted[damageType.Key][damageChangeType.Key].Add(reagent.Key); + + } + + healthChangeReagentsSorted[damageType.Key][damageChangeType.Key].Sort(Comparer.Create((s1, s2) => + -healthChangeReagents[damageType.Key][damageChangeType.Key][s1].CompareTo(healthChangeReagents[damageType.Key][damageChangeType.Key][s2]))); + } + } + + + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + + file.Write(JsonSerializer.Serialize(healthChangeReagentsSorted, serializeOptions)); + } +} + diff --git a/Content.Server/Corvax/GuideGenerator/HeatableRecipeEntry.cs b/Content.Server/Corvax/GuideGenerator/HeatableRecipeEntry.cs new file mode 100644 index 00000000000..73f22f5e644 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/HeatableRecipeEntry.cs @@ -0,0 +1,93 @@ +using System.Text.Json.Serialization; +using Robust.Shared.Prototypes; +using Content.Shared.Construction; +using Content.Shared.Construction.Prototypes; +using Content.Shared.Construction.Steps; +using Robust.Server.GameObjects; + +namespace Content.Server.GuideGenerator; + +public sealed class HeatableRecipeEntry +{ + /// + /// Id of recipe + /// + [JsonPropertyName("id")] + public string Id { get; } + + /// + /// Human-readable name of recipe. + /// Should automatically be localized by default + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Type of recipe + /// + [JsonPropertyName("type")] + public string Type { get; } + + /// + /// Temp, required for "input" thing to become "result" thing + /// + [JsonPropertyName("minTemp")] + public float MinTemp { get; } + + /// + /// Item that will be transformed into something with enough temp + /// + [JsonPropertyName("input")] + public string Input { get; } + + /// + /// Result of a recipe. + /// If it is null then recipe does not exist or we could not get recipe info. + /// + [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 + ServerEntityManager 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 = TextTools.TextTools.CapitalizeString(entityPrototype.Name); + Id = entityPrototype.ID; + Type = "heatableRecipes"; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/MealsRecipesJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/MealsRecipesJsonGenerator.cs new file mode 100644 index 00000000000..fde7a38fe2d --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/MealsRecipesJsonGenerator.cs @@ -0,0 +1,128 @@ +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 MealsRecipesJsonGenerator +{ + public static void PublishJson(StreamWriter file) + { + var prototype = IoCManager.Resolve(); + var entities = prototype.EnumeratePrototypes(); + var constructable = prototype.EnumeratePrototypes(); + var output = new Dictionary(); + + var microwaveRecipes = + prototype + .EnumeratePrototypes() + .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)) + .Where(x => x.Result != null) + .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*|[Tt]ortilla*|[Ee]gg*").Success)) // we only need recipes that has "bacon", "steak", "pizza" "tortilla" and "egg" 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(), @"(? x.Components.ContainsKey("Construction")) + .ToList(); + + var entityGraphs = new Dictionary(); // 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() + .Select(x => new ReactionEntry(x)) + .ToList(); + + + var mixableRecipes = new Dictionary>(); // 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(); + 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)); + } +} diff --git a/Content.Server/Corvax/GuideGenerator/MicrowaveRecipeEntry.cs b/Content.Server/Corvax/GuideGenerator/MicrowaveRecipeEntry.cs new file mode 100644 index 00000000000..a38dcfd6c6a --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/MicrowaveRecipeEntry.cs @@ -0,0 +1,71 @@ +using System.Linq; +using System.Text.Json.Serialization; +using Content.Shared.Kitchen; + +namespace Content.Server.GuideGenerator; + +public sealed class MicrowaveRecipeEntry +{ + /// + /// Id of recipe + /// + [JsonPropertyName("id")] + public string Id { get; } + + /// + /// Human-readable name of recipe. + /// Should automatically be localized by default + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Type of recipe + /// + [JsonPropertyName("type")] + public string Type { get; } + + /// + /// Time to cook something (for microwave recipes) + /// + [JsonPropertyName("time")] + public uint Time { get; } + + /// + /// Solids required to cook something + /// + [JsonPropertyName("solids")] + public Dictionary Solids { get; } + + /// + /// Reagents required to cook something + /// + [JsonPropertyName("reagents")] + public Dictionary Reagents { get; } + + /// + /// Result of a recipe + /// + [JsonPropertyName("result")] + public string Result { get; } + + + public MicrowaveRecipeEntry(FoodRecipePrototype proto) + { + Id = proto.ID; + Name = TextTools.TextTools.CapitalizeString(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; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/MixingCategoryEntry.cs b/Content.Server/Corvax/GuideGenerator/MixingCategoryEntry.cs new file mode 100644 index 00000000000..99d1c02041b --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/MixingCategoryEntry.cs @@ -0,0 +1,19 @@ +using Content.Shared.Chemistry.Reaction; +using System.Text.Json.Serialization; + +namespace Content.Server.Corvax.GuideGenerator; + +public sealed class MixingCategoryEntry +{ + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("id")] + public string Id { get; } + + public MixingCategoryEntry(MixingCategoryPrototype proto) + { + Name = Loc.GetString(proto.VerbText); + Id = proto.ID; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/ReactionJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/ReactionJsonGenerator.cs new file mode 100644 index 00000000000..c81448353a1 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ReactionJsonGenerator.cs @@ -0,0 +1,35 @@ +using Content.Server.Corvax.GuideGenerator; +using Content.Shared.Chemistry.Reaction; +using Robust.Shared.Prototypes; + +namespace Content.Server.GuideGenerator; +public sealed partial class ReactionJsonGenerator +{ + [ValidatePrototypeId] + private const string DefaultMixingCategory = "DummyMix"; + + private static void AddMixingCategories(Dictionary reactions, IPrototypeManager prototype) + { + foreach (var reaction in reactions) + { + var reactionPrototype = prototype.Index(reaction.Key); + var mixingCategories = new List(); + if (reactionPrototype.MixingCategories != null) + { + foreach (var category in reactionPrototype.MixingCategories) + { + mixingCategories.Add(prototype.Index(category)); + } + } + else + { + mixingCategories.Add(prototype.Index(DefaultMixingCategory)); + } + + foreach (var mixingCategory in mixingCategories) + { + reactions[reaction.Key].MixingCategories.Add(new MixingCategoryEntry(mixingCategory)); + } + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/ReagentEffectEntry.cs b/Content.Server/Corvax/GuideGenerator/ReagentEffectEntry.cs new file mode 100644 index 00000000000..680f307ee19 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ReagentEffectEntry.cs @@ -0,0 +1,41 @@ +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; +using System.Text.Json.Serialization; +using Content.Server.Chemistry.ReactionEffects; + +namespace Content.Server.Corvax.GuideGenerator; +public sealed class ReagentEffectEntry +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("description")] + public string Description { get; } + + public ReagentEffectEntry(ReagentEffect proto) + { + var prototype = IoCManager.Resolve(); + var entSys = IoCManager.Resolve(); + + Id = proto.GetType().Name; + Description = GuidebookEffectDescriptionToWeb(proto.GuidebookEffectDescription(prototype, entSys) ?? ""); + } + + private string GuidebookEffectDescriptionToWeb(string guideBookText) + { + guideBookText = guideBookText.Replace("[", "<"); + guideBookText = guideBookText.Replace("]", ">"); + guideBookText = guideBookText.Replace("color", "span"); + + while (guideBookText.IndexOf("", first); + var replacementString = guideBookText.Substring(first, last - first); + var color = replacementString.Substring(1); + guideBookText = guideBookText.Replace(replacementString, string.Format(" style=\"color: {0};\"", color)); + } + + return guideBookText; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/ReagentEffectsEntry.cs b/Content.Server/Corvax/GuideGenerator/ReagentEffectsEntry.cs new file mode 100644 index 00000000000..3b96c3bd84e --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ReagentEffectsEntry.cs @@ -0,0 +1,20 @@ +using Content.Shared.FixedPoint; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Content.Server.Corvax.GuideGenerator; +public sealed class ReagentEffectsEntry +{ + [JsonPropertyName("rate")] + public FixedPoint2 MetabolismRate { get; } = FixedPoint2.New(0.5f); + + [JsonPropertyName("effects")] + public List Effects { get; } = new(); + + public ReagentEffectsEntry(Shared.Chemistry.Reagent.ReagentEffectsEntry proto) + { + MetabolismRate = proto.MetabolismRate; + Effects = proto.Effects.Select(x => new ReagentEffectEntry(x)).ToList(); + } + +} diff --git a/Content.Server/Corvax/GuideGenerator/SliceRecipeEntry.cs b/Content.Server/Corvax/GuideGenerator/SliceRecipeEntry.cs new file mode 100644 index 00000000000..a4cde7d6784 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/SliceRecipeEntry.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; +using Robust.Shared.Prototypes; +using Content.Server.Nutrition.Components; + +namespace Content.Server.GuideGenerator; + +public sealed class SliceRecipeEntry +{ + /// + /// Id of sliceable item + /// + [JsonPropertyName("id")] + public string Id { get; } + + /// + /// Human-readable name of recipe. + /// Should automatically be localized by default + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Type of recipe + /// + [JsonPropertyName("type")] + public string Type { get; } + + /// + /// Item that will be sliced into something + /// + [JsonPropertyName("input")] + public string Input { get; } + + /// + /// Result of a recipe + /// + [JsonPropertyName("result")] + public string Result { get; } + + /// + /// Count of result item + /// + [JsonPropertyName("count")] + public int Count { get; } + + + public SliceRecipeEntry(EntityPrototype proto) + { + Id = proto.ID; + Name = TextTools.TextTools.CapitalizeString(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; + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/TextTools.cs b/Content.Server/Corvax/GuideGenerator/TextTools.cs new file mode 100644 index 00000000000..2e441f05675 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/TextTools.cs @@ -0,0 +1,25 @@ +namespace Content.Server.GuideGenerator.TextTools; + +public sealed class TextTools +{ + /// + /// Capitalizes first letter of given string. + /// + /// String to capitalize + /// String with capitalized first letter + public static string CapitalizeString(string str) + { + if (str.Length > 1) + { + return char.ToUpper(str[0]) + str.Remove(0, 1); + } + else if (str.Length == 1) + { + return char.ToUpper(str[0]).ToString(); + } + else + { + return str; + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/ToolRecipeEntry.cs b/Content.Server/Corvax/GuideGenerator/ToolRecipeEntry.cs new file mode 100644 index 00000000000..23362457a64 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ToolRecipeEntry.cs @@ -0,0 +1,96 @@ +using System.Text.Json.Serialization; +using Robust.Shared.Prototypes; +using Content.Shared.Construction; +using Content.Shared.Construction.Prototypes; +using Content.Shared.Construction.Steps; +using Robust.Server.GameObjects; + +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 :) +{ + + /// + /// Id of recipe + /// + [JsonPropertyName("id")] + public string Id { get; } + + /// + /// Human-readable name of recipe. + /// Should automatically be localized by default + /// + [JsonPropertyName("name")] + public string Name { get; } + + /// + /// Type of recipe + /// + [JsonPropertyName("type")] + public string Type { get; } + + /// + /// Type of tool that is used to convert input into result + /// + [JsonPropertyName("tool")] + public string? Tool { get; } + + /// + /// Item that will be transformed into something with enough temp + /// + [JsonPropertyName("input")] + public string Input { get; } + + /// + /// Result of a recipe. + /// If it is null then recipe does not exist or we could not get recipe info. + /// + [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 + ServerEntityManager 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 = TextTools.TextTools.CapitalizeString(entityPrototype.Name); + Id = entityPrototype.ID; + Type = "toolmadeRecipes"; + } +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 6fb865b0b29..e7d2e64798b 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -4,6 +4,7 @@ using Content.Server.Administration.Managers; using Content.Server.Afk; using Content.Server.Chat.Managers; +using Content.Server.Corvax.GuideGenerator; using Content.Server.Corvax.TTS; using Content.Server.Connection; using Content.Server.JoinQueue; @@ -140,6 +141,17 @@ public override void PostInit() file = resourceManager.UserData.OpenWriteText(resPath.WithName("react_" + dest)); ReactionJsonGenerator.PublishJson(file); file.Flush(); + // Corvax-Wiki-Start + file = resourceManager.UserData.OpenWriteText(resPath.WithName("entity_" + dest)); + EntityJsonGenerator.PublishJson(file); + file.Flush(); + file = resourceManager.UserData.OpenWriteText(resPath.WithName("mealrecipes_" + dest)); + MealsRecipesJsonGenerator.PublishJson(file); + file.Flush(); + file = resourceManager.UserData.OpenWriteText(resPath.WithName("healthchangereagents_" + dest)); + HealthChangeReagentsJsonGenerator.PublishJson(file); + file.Flush(); + // Corvax-Wiki-End IoCManager.Resolve().Shutdown("Data generation done"); } else diff --git a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs index 7e107ce1a5a..f57c63a1d07 100644 --- a/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ChemistryJsonGenerator.cs @@ -38,6 +38,7 @@ public static void PublishJson(StreamWriter file) var serializeOptions = new JsonSerializerOptions { WriteIndented = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals, // Corvax-Wiki Converters = { new UniversalJsonConverter(), diff --git a/Content.Server/GuideGenerator/ReactionJsonGenerator.cs b/Content.Server/GuideGenerator/ReactionJsonGenerator.cs index 123dd5f8eb3..a8f312275b8 100644 --- a/Content.Server/GuideGenerator/ReactionJsonGenerator.cs +++ b/Content.Server/GuideGenerator/ReactionJsonGenerator.cs @@ -7,7 +7,7 @@ namespace Content.Server.GuideGenerator; -public sealed class ReactionJsonGenerator +public sealed partial class ReactionJsonGenerator { public static void PublishJson(StreamWriter file) { @@ -19,9 +19,14 @@ public static void PublishJson(StreamWriter file) .Select(x => new ReactionEntry(x)) .ToDictionary(x => x.Id, x => x); + // Corvax-Wiki-Start + if (reactions is not null) AddMixingCategories(reactions, prototype); + // Corvax-Wiki-End + var serializeOptions = new JsonSerializerOptions { WriteIndented = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals, // Corvax-Wiki Converters = { new UniversalJsonConverter(), diff --git a/Content.Server/GuideGenerator/ReagentEntry.cs b/Content.Server/GuideGenerator/ReagentEntry.cs index ab9e7582060..a59570da851 100644 --- a/Content.Server/GuideGenerator/ReagentEntry.cs +++ b/Content.Server/GuideGenerator/ReagentEntry.cs @@ -1,10 +1,8 @@ using System.Linq; using System.Text.Json.Serialization; -using Content.Server.Body.Components; -using Content.Shared.Body.Prototypes; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; -using Robust.Shared.Prototypes; +using Content.Server.Corvax.GuideGenerator; namespace Content.Server.GuideGenerator; @@ -28,11 +26,14 @@ public sealed class ReagentEntry [JsonPropertyName("color")] public string SubstanceColor { get; } + [JsonPropertyName("textColor")] + public string TextColor { get; } // Corvax-Wiki + [JsonPropertyName("recipes")] public List Recipes { get; } = new(); [JsonPropertyName("metabolisms")] - public Dictionary? Metabolisms { get; } + public Dictionary? Metabolisms { get; } // Corvax-Wiki public ReagentEntry(ReagentPrototype proto) { @@ -42,7 +43,17 @@ public ReagentEntry(ReagentPrototype proto) Description = proto.LocalizedDescription; PhysicalDescription = proto.LocalizedPhysicalDescription; SubstanceColor = proto.SubstanceColor.ToHex(); - Metabolisms = proto.Metabolisms?.ToDictionary(x => x.Key.Id, x => x.Value); + + // Corvax-Wiki-Start + var r = proto.SubstanceColor.R; + var g = proto.SubstanceColor.G; + var b = proto.SubstanceColor.B; + TextColor = (0.2126f * r + 0.7152f * g + 0.0722f * b > 0.5 + ? Color.Black + : Color.White).ToHex(); + + Metabolisms = proto.Metabolisms?.ToDictionary(x => x.Key.Id, x => new Corvax.GuideGenerator.ReagentEffectsEntry(x.Value)); + // Corvax-Wiki-End } } @@ -60,7 +71,24 @@ public sealed class ReactionEntry [JsonPropertyName("products")] public Dictionary Products { get; } + // Corvax-Wiki-Start + [JsonPropertyName("mixingCategories")] + public List MixingCategories { get; } = new(); + + [JsonPropertyName("minTemp")] + public float MinTemp { get; } + + [JsonPropertyName("maxTemp")] + public float MaxTemp { get; } + + [JsonPropertyName("hasMax")] + public bool HasMax { get; } + [JsonPropertyName("effects")] + public List ExportEffects { get; } = new(); + + [JsonIgnore] + // Corvax-Wiki-End public List Effects { get; } public ReactionEntry(ReactionPrototype proto) @@ -76,6 +104,13 @@ public ReactionEntry(ReactionPrototype proto) .Select(x => KeyValuePair.Create(x.Key, x.Value.Float())) .ToDictionary(x => x.Key, x => x.Value); Effects = proto.Effects; + + // Corvax-Wiki-Start + ExportEffects = proto.Effects.Select(x => new ReagentEffectEntry(x)).ToList(); + MinTemp = proto.MinimumTemperature; + MaxTemp = proto.MaximumTemperature; + HasMax = !float.IsPositiveInfinity(MaxTemp); + // Corvax-Wiki-End } } diff --git a/Content.Server/GuideGenerator/UniversalJsonConverter.cs b/Content.Server/GuideGenerator/UniversalJsonConverter.cs index bcf9702c1c0..a17855c548b 100644 --- a/Content.Server/GuideGenerator/UniversalJsonConverter.cs +++ b/Content.Server/GuideGenerator/UniversalJsonConverter.cs @@ -73,6 +73,11 @@ public override void Write(Utf8JsonWriter writer, T obj, JsonSerializerOptions o // If the field has a [JsonIgnore] attribute, skip it if (Attribute.GetCustomAttribute(prop, typeof(JsonIgnoreAttribute), true) != null) continue; + // If GetIndexParameters().Length is not 0 then it means that property is indexed + // And since we cannot get its values without passing index (which type can LITERALLY BE ANYTHING) then let's just skip it + // Yeah, i know that this will lead to a potential data loss, but what i can do about it? + if (prop.GetIndexParameters().Length != 0) continue; // Corvax-Wiki + // If the property has a [JsonPropertyName] attribute, get the property name. Otherwise, use the property name. JsonPropertyNameAttribute? attr = (JsonPropertyNameAttribute?) Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute), true); string name = attr == null ? prop.Name : attr.Name; diff --git a/Resources/Locale/en-US/_LostParadise/update20.ftl b/Resources/Locale/en-US/_LostParadise/update20.ftl index 58ea4e6451b..7141e603b8d 100644 --- a/Resources/Locale/en-US/_LostParadise/update20.ftl +++ b/Resources/Locale/en-US/_LostParadise/update20.ftl @@ -16,3 +16,4 @@ reagent-desc-red-bool = Окрыляет! id-card-access-level-expeditor = Исследователь chat-manager-send-ooc-admin-wrap-message = OOC: [bold]\[{ $adminTitle }\] { $playerName }:[/bold] { $message } ui-options-function-lpp-smart-equip-neck = Умная экипировка на шею +species-name-shark = Акулоид diff --git a/Resources/Locale/en-US/deltav/guidebook/justice.ftl b/Resources/Locale/en-US/deltav/guidebook/justice.ftl new file mode 100644 index 00000000000..911c86ca05e --- /dev/null +++ b/Resources/Locale/en-US/deltav/guidebook/justice.ftl @@ -0,0 +1 @@ +guide-entry-justice = Justice diff --git a/Resources/Locale/ru-RU/_LostParadise/update20.ftl b/Resources/Locale/ru-RU/_LostParadise/update20.ftl index 58ea4e6451b..7141e603b8d 100644 --- a/Resources/Locale/ru-RU/_LostParadise/update20.ftl +++ b/Resources/Locale/ru-RU/_LostParadise/update20.ftl @@ -16,3 +16,4 @@ reagent-desc-red-bool = Окрыляет! id-card-access-level-expeditor = Исследователь chat-manager-send-ooc-admin-wrap-message = OOC: [bold]\[{ $adminTitle }\] { $playerName }:[/bold] { $message } ui-options-function-lpp-smart-equip-neck = Умная экипировка на шею +species-name-shark = Акулоид diff --git a/Resources/Locale/ru-RU/deltav/guidebook/justice.ftl b/Resources/Locale/ru-RU/deltav/guidebook/justice.ftl new file mode 100644 index 00000000000..aec25b836ce --- /dev/null +++ b/Resources/Locale/ru-RU/deltav/guidebook/justice.ftl @@ -0,0 +1 @@ +guide-entry-justice = Правосудие diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_lostparadise/entities/mobs/species/shark/entities/mobs/species/Shark.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_lostparadise/entities/mobs/species/shark/entities/mobs/species/Shark.ftl index a52eca131c0..279102003a1 100644 --- a/Resources/Locale/ru-RU/ss14-ru/prototypes/_lostparadise/entities/mobs/species/shark/entities/mobs/species/Shark.ftl +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_lostparadise/entities/mobs/species/shark/entities/mobs/species/Shark.ftl @@ -1,4 +1,4 @@ -ent-LPPBaseMobShark = Shark McHands +ent-LPPBaseMobShark = Акула МакХендс + .desc = { ent-BaseMobSpeciesOrganic.desc } +ent-LPPMobSharkDummy = Акула МакХендс .desc = { ent-BaseMobSpeciesOrganic.desc } -ent-LPPMobSharkDummy = Shark McHands - .desc = A dummy shark meant to be used in character setup. diff --git a/Resources/Prototypes/_LostParadise/Entities/Mobs/Species/Shark/Species/Shark.yml b/Resources/Prototypes/_LostParadise/Entities/Mobs/Species/Shark/Species/Shark.yml index 9947285b5fa..6c6dad20ef4 100644 --- a/Resources/Prototypes/_LostParadise/Entities/Mobs/Species/Shark/Species/Shark.yml +++ b/Resources/Prototypes/_LostParadise/Entities/Mobs/Species/Shark/Species/Shark.yml @@ -1,6 +1,6 @@ - type: species id: SharkSpecies - name: "Shark" + name: species-name-shark roundStart: true #can player role prototype: LPPMobShark sprites: MobSharkSprites diff --git a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword.png b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword.png index e7d9fcbcbff..0a52bcd0591 100644 Binary files a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword.png and b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword_blade.png b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword_blade.png index 3c03bbc7891..714e7408332 100644 Binary files a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword_blade.png and b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/e_sword_blade.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/meta.json index 58c2a67aac6..39f1c6da957 100644 --- a/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Melee/e_dagger.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation, edited by Prazat", "size": { "x": 32, "y": 32