From f46bb301fbf03b3b3bb43d87bb96e4e0913658e2 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:07:51 +1000 Subject: [PATCH] Fix lathe arbitrage test (#34449) * Fix lathe arbitrage test * Add refinables * nullable * nullable2 * Fix merge * Ignore failures --- .../Tests/MaterialArbitrageTest.cs | 144 +++++++++++++++--- Content.Server/Lathe/LatheSystem.cs | 19 +-- Content.Shared/Lathe/LatheComponent.cs | 12 +- Content.Shared/Lathe/SharedLatheSystem.cs | 19 +++ 4 files changed, 156 insertions(+), 38 deletions(-) diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs index e6422f0ec4..4b020e9850 100644 --- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs +++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs @@ -1,12 +1,13 @@ +#nullable enable using System.Collections.Generic; using Content.Server.Cargo.Systems; using Content.Server.Construction.Completions; using Content.Server.Construction.Components; using Content.Server.Destructible; using Content.Server.Destructible.Thresholds.Behaviors; +using Content.Server.Lathe; using Content.Server.Stack; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Construction.Components; using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Steps; using Content.Shared.FixedPoint; @@ -14,10 +15,9 @@ using Content.Shared.Lathe; using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using Content.Shared.Stacks; +using Content.Shared.Tools.Components; using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests; @@ -28,6 +28,21 @@ namespace Content.IntegrationTests.Tests; [TestFixture] public sealed class MaterialArbitrageTest { + // These recipes are currently broken and need fixing. You should not be adding to these sets. + private readonly HashSet _destructionArbitrageIgnore = + [ + "BaseChemistryEmptyVial", "DrinkShotGlass", "Beaker", "SodiumLightTube", "DrinkGlassCoupeShaped", + "LedLightBulb", "ExteriorLightTube", "LightTube", "DrinkGlass", "DimLightBulb", "LightBulb", "LedLightTube", + "SheetRGlass1", "ChemistryEmptyBottle01", "WarmLightBulb", + ]; + + private readonly HashSet _compositionArbitrageIgnore = + [ + "FoodPlateSmall", "AirTank", "FoodPlateTin", "FoodPlateMuffinTin", "WeaponCapacitorRechargerCircuitboard", + "WeaponCapacitorRechargerCircuitboard", "BorgChargerCircuitboard", "BorgChargerCircuitboard", "FoodPlate", + "CellRechargerCircuitboard", "CellRechargerCircuitboard", + ]; + [Test] public async Task NoMaterialArbitrage() { @@ -38,13 +53,12 @@ public sealed class MaterialArbitrageTest await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var protoManager = server.ResolveDependency(); var pricing = entManager.System(); var stackSys = entManager.System(); var mapSystem = server.System(); - var latheSys = server.System(); + var latheSys = server.System(); var compFact = server.ResolveDependency(); Assert.That(mapSystem.IsInitialized(testMap.MapId)); @@ -53,13 +67,24 @@ public sealed class MaterialArbitrageTest var compositionName = compFact.GetComponentName(typeof(PhysicalCompositionComponent)); var materialName = compFact.GetComponentName(typeof(MaterialComponent)); var destructibleName = compFact.GetComponentName(typeof(DestructibleComponent)); + var refinableName = compFact.GetComponentName(typeof(ToolRefinableComponent)); // get the inverted lathe recipe dictionary var latheRecipes = latheSys.InverseRecipes; - // Lets assume the possible lathe for resource multipliers: - // TODO: each recipe can technically have its own cost multiplier associated with it, so this test needs redone to factor that in. - var multiplier = MathF.Pow(0.85f, 3); + // Find the lowest multiplier / optimal lathe that can be used to construct a recipie. + var minMultiplier = new Dictionary, float>(); + + foreach (var (_, lathe) in pair.GetPrototypesWithComponent()) + { + foreach (var recipe in latheSys.GetAllPossibleRecipes(lathe)) + { + if (!minMultiplier.TryGetValue(recipe, out var min)) + min = 1; + + minMultiplier[recipe] = Math.Min(min, lathe.MaterialUseMultiplier); + } + } // create construction dictionary Dictionary constructionRecipes = new(); @@ -122,6 +147,65 @@ public sealed class MaterialArbitrageTest Dictionary Ents, Dictionary Mats)> spawnedOnDestroy = new(); + // cache the compositions of entities + // If the entity is refineable (i.e. glass shared can be turned into glass, we take the greater of the two compositions. + Dictionary> compositions = new(); + foreach (var proto in protoManager.EnumeratePrototypes()) + { + Dictionary? baseComposition = null; + + if (proto.Components.ContainsKey(materialName) + && proto.Components.TryGetValue(compositionName, out var compositionReg)) + { + var compositionComp = (PhysicalCompositionComponent)compositionReg.Component; + baseComposition = compositionComp.MaterialComposition; + + } + + if (!proto.Components.TryGetValue(refinableName, out var refinableReg)) + { + if (baseComposition != null) + compositions[proto.ID] = new(baseComposition); + continue; + } + + var composition = new Dictionary(); + compositions.Add(proto.ID, composition); + + var refinable = (ToolRefinableComponent)refinableReg.Component; + foreach (var refineResult in refinable.RefineResult) + { + if (refineResult.PrototypeId == null) + continue; + + var refineProto = protoManager.Index(refineResult.PrototypeId.Value); + if (!refineProto.Components.ContainsKey(materialName)) + continue; + + if (!refineProto.Components.TryGetValue(compositionName, out var refinedCompositionReg)) + continue; + + var refinedCompositionComp = (PhysicalCompositionComponent)refinedCompositionReg.Component; + + // This assumes refine results do not have complex spawn behaviours like exclusive groups. + var quantity = refineResult.MaxAmount; + + foreach (var (matId, amount) in refinedCompositionComp.MaterialComposition) + { + composition[matId] = quantity * amount + composition.GetValueOrDefault(matId); + } + } + + if (baseComposition == null) + continue; + + // If the un-refined material quantity is greater than the refined quantity, we use that instead. + foreach (var (matId, amount) in baseComposition) + { + composition[matId] = Math.Max(amount, composition.GetValueOrDefault(matId)); + } + } + // Here we get the set of entities/materials spawned when destroying an entity. foreach (var proto in protoManager.EnumeratePrototypes()) { @@ -151,16 +235,10 @@ public sealed class MaterialArbitrageTest { spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + value.Max; - var spawnProto = protoManager.Index(key); - - // get the amount of each material included in the entity - - if (!spawnProto.Components.ContainsKey(materialName) || - !spawnProto.Components.TryGetValue(compositionName, out var compositionReg)) + if (!compositions.TryGetValue(key, out var composition)) continue; - var mat = (PhysicalCompositionComponent) compositionReg.Component; - foreach (var (matId, amount) in mat.MaterialComposition) + foreach (var (matId, amount) in composition) { spawnedMats[matId] = value.Max * amount + spawnedMats.GetValueOrDefault(matId); } @@ -173,10 +251,13 @@ public sealed class MaterialArbitrageTest } // This is the main loop where we actually check for destruction arbitrage - Assert.Multiple(async () => + await Assert.MultipleAsync(async () => { foreach (var (id, (spawnedEnts, spawnedMats)) in spawnedOnDestroy) { + if (_destructionArbitrageIgnore.Contains(id)) + continue; + // Check cargo sell price // several constructible entities have no sell price // also this test only really matters if the entity is also purchaseable.... eh.. @@ -190,6 +271,11 @@ public sealed class MaterialArbitrageTest { foreach (var recipe in recipes) { + if (!minMultiplier.TryGetValue(recipe, out var multiplier)) + { + server.Log.Info($"Unused lathe recipe? {recipe.ID}?"); + continue; + } foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); @@ -231,6 +317,9 @@ public sealed class MaterialArbitrageTest var edge = cur.GetEdge(node.Name); cur = node; + if (edge == null) + continue; + foreach (var completion in edge.Completed) { if (completion is not SpawnPrototype spawnCompletion) @@ -253,9 +342,9 @@ public sealed class MaterialArbitrageTest } // This is functionally the same loop as before, but now testing deconstruction rather than destruction. - // This is pretty braindead. In principle construction graphs can have loops and whatnot. + // This is pretty brain-dead. In principle construction graphs can have loops and whatnot. - Assert.Multiple(async () => + await Assert.MultipleAsync(async () => { foreach (var (id, deconstructedMats) in deconstructionMaterials) { @@ -270,6 +359,11 @@ public sealed class MaterialArbitrageTest { foreach (var recipe in recipes) { + if (!minMultiplier.TryGetValue(recipe, out var multiplier)) + { + server.Log.Info($"Unused lathe recipe? {recipe.ID}?"); + continue; + } foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); @@ -291,7 +385,7 @@ public sealed class MaterialArbitrageTest } }); - // create phyiscal composition dictionary + // create physical composition dictionary // this doesn't account for the chemicals in the composition Dictionary physicalCompositions = new(); foreach (var proto in protoManager.EnumeratePrototypes()) @@ -308,10 +402,13 @@ public sealed class MaterialArbitrageTest // This is functionally the same loop as before, but now testing composition rather than destruction or deconstruction. // This doesn't take into account chemicals generated when deconstructing. Maybe it should. - Assert.Multiple(async () => + await Assert.MultipleAsync(async () => { foreach (var (id, compositionComponent) in physicalCompositions) { + if (_compositionArbitrageIgnore.Contains(id)) + continue; + // Check cargo sell price var materialPrice = await GetDeconstructedPrice(compositionComponent.MaterialComposition); var chemicalPrice = await GetChemicalCompositionPrice(compositionComponent.ChemicalComposition); @@ -325,6 +422,11 @@ public sealed class MaterialArbitrageTest { foreach (var recipe in recipes) { + if (!minMultiplier.TryGetValue(recipe, out var multiplier)) + { + server.Log.Info($"Unused lathe recipe? {recipe.ID}?"); + continue; + } foreach (var (matId, amount) in recipe.Materials) { var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier); diff --git a/Content.Server/Lathe/LatheSystem.cs b/Content.Server/Lathe/LatheSystem.cs index 4851f6b63d..0212bb6eeb 100644 --- a/Content.Server/Lathe/LatheSystem.cs +++ b/Content.Server/Lathe/LatheSystem.cs @@ -61,7 +61,6 @@ namespace Content.Server.Lathe /// Per-tick cache /// private readonly List _environments = new(); - private readonly HashSet> _availableRecipes = new(); public override void Initialize() { @@ -162,12 +161,8 @@ namespace Content.Server.Lathe public List> GetAvailableRecipes(EntityUid uid, LatheComponent component, bool getUnavailable = false) { - _availableRecipes.Clear(); - AddRecipesFromPacks(_availableRecipes, component.StaticPacks); - var ev = new LatheGetRecipesEvent(uid, getUnavailable) - { - Recipes = _availableRecipes - }; + var ev = new LatheGetRecipesEvent((uid, component), getUnavailable); + AddRecipesFromPacks(ev.Recipes, component.StaticPacks); RaiseLocalEvent(uid, ev); return ev.Recipes.ToList(); } @@ -290,7 +285,7 @@ namespace Content.Server.Lathe var pack = _proto.Index(id); foreach (var recipe in pack.Recipes) { - if (args.getUnavailable || database.UnlockedRecipes.Contains(recipe)) + if (args.GetUnavailable || database.UnlockedRecipes.Contains(recipe)) args.Recipes.Add(recipe); } } @@ -298,10 +293,8 @@ namespace Content.Server.Lathe private void OnGetRecipes(EntityUid uid, TechnologyDatabaseComponent component, LatheGetRecipesEvent args) { - if (uid != args.Lathe || !TryComp(uid, out var latheComponent)) - return; - - AddRecipesFromDynamicPacks(ref args, component, latheComponent.DynamicPacks); + if (uid == args.Lathe) + AddRecipesFromDynamicPacks(ref args, component, args.Comp.DynamicPacks); } private void GetEmagLatheRecipes(EntityUid uid, EmagLatheRecipesComponent component, LatheGetRecipesEvent args) @@ -309,7 +302,7 @@ namespace Content.Server.Lathe if (uid != args.Lathe) return; - if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction)) + if (!args.GetUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction)) return; AddRecipesFromPacks(args.Recipes, component.EmagStaticPacks); diff --git a/Content.Shared/Lathe/LatheComponent.cs b/Content.Shared/Lathe/LatheComponent.cs index aaf273e0fe..80f4f62a31 100644 --- a/Content.Shared/Lathe/LatheComponent.cs +++ b/Content.Shared/Lathe/LatheComponent.cs @@ -21,6 +21,9 @@ namespace Content.Shared.Lathe /// [DataField] public List> DynamicPacks = new(); + // Note that this shouldn't be modified dynamically. + // I.e., this + the static recipies should represent all recipies that the lathe can ever make + // Otherwise the material arbitrage test and/or LatheSystem.GetAllBaseRecipes needs to be updated /// /// The lathe's construction queue @@ -81,15 +84,16 @@ namespace Content.Shared.Lathe public sealed class LatheGetRecipesEvent : EntityEventArgs { public readonly EntityUid Lathe; + public readonly LatheComponent Comp; - public bool getUnavailable; + public bool GetUnavailable; public HashSet> Recipes = new(); - public LatheGetRecipesEvent(EntityUid lathe, bool forced) + public LatheGetRecipesEvent(Entity lathe, bool forced) { - Lathe = lathe; - getUnavailable = forced; + (Lathe, Comp) = lathe; + GetUnavailable = forced; } } diff --git a/Content.Shared/Lathe/SharedLatheSystem.cs b/Content.Shared/Lathe/SharedLatheSystem.cs index ae5519d16c..524d83fd84 100644 --- a/Content.Shared/Lathe/SharedLatheSystem.cs +++ b/Content.Shared/Lathe/SharedLatheSystem.cs @@ -34,6 +34,25 @@ public abstract class SharedLatheSystem : EntitySystem BuildInverseRecipeDictionary(); } + /// + /// Get the set of all recipes that a lathe could possibly ever create (e.g., if all techs were unlocked). + /// + public HashSet> GetAllPossibleRecipes(LatheComponent component) + { + var recipes = new HashSet>(); + foreach (var pack in component.StaticPacks) + { + recipes.UnionWith(_proto.Index(pack).Recipes); + } + + foreach (var pack in component.DynamicPacks) + { + recipes.UnionWith(_proto.Index(pack).Recipes); + } + + return recipes; + } + /// /// Add every recipe in the list of recipe packs to a single hashset. ///