From da067a34a28b2aefe8b3adbe2f4bddb6f8957045 Mon Sep 17 00:00:00 2001 From: soul Date: Tue, 27 Oct 2026 10:06:15 +0100 Subject: [PATCH] Economy checks: the consumer walk follows upkeep edges; pin upkeep into a consumer good and a role filler Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/EconomyChecks.cs | 4 ++-- .../EconomyChecksTests.cs | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/ServerShared/EconomyChecks.cs b/Assets/Scripts/ServerShared/EconomyChecks.cs index 20bfdefa4..4e51bfd4a 100644 --- a/Assets/Scripts/ServerShared/EconomyChecks.cs +++ b/Assets/Scripts/ServerShared/EconomyChecks.cs @@ -411,7 +411,7 @@ public static class EconomyChecks // A processed good owes a consumer use when it is shared (rulings consumer-demand-shared-only, draw-is-one-use). Its uses are // the gear or hull roles, and the rounds some weapon draws, that the goods downstream of it (itself included) fill or are; a // good used as upkeep is shared too. Shared means two or more such uses, or upkeep. A shared good must lead, through the - // chain, into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly. + // chain (an input edge or an upkeep edge), into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly. // Derived from the recipe graph on every check; depth and distance play no part. Raw goods and the gate count for nothing. public static List ConsumerDemand(CultCache catalog) { @@ -424,7 +424,7 @@ public static class EconomyChecks .SelectMany(r => Recipes.RolesOf(catalog.Get(r.Output))).Select(role => role.Name), StringComparer.Ordinal); var upkeep = new HashSet(recipes.Values.SelectMany(r => r.Upkeep).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal); var takenBy = recipes.Values.Where(IsCommodityRecipe) - .SelectMany(r => r.Inputs.Select(input => (Input: input.Commodity.Key.Value, Output: r.Output.Key.Value))) + .SelectMany(r => r.Inputs.Concat(r.Upkeep).Select(input => (Input: input.Commodity.Key.Value, Output: r.Output.Key.Value))) .ToLookup(edge => edge.Input, edge => edge.Output, StringComparer.Ordinal); var faults = new List(); foreach (var key in recipes.Keys.OrderBy(k => k, StringComparer.Ordinal)) diff --git a/tests/Aetheria.Shared.Tests/EconomyChecksTests.cs b/tests/Aetheria.Shared.Tests/EconomyChecksTests.cs index a0ae40c86..539832ba7 100644 --- a/tests/Aetheria.Shared.Tests/EconomyChecksTests.cs +++ b/tests/Aetheria.Shared.Tests/EconomyChecksTests.cs @@ -383,13 +383,32 @@ public sealed class EconomyChecksTests // Fuel is upkeep itself; coke is shared because a good it leads into is. var faults = EconomyChecks.ConsumerDemand(f.Cache); Assert.Equal(new[] { "recipe:commodity:coke", "recipe:commodity:fuel" }, faults.Select(x => x.Record)); - Assert.Contains("'fuel' leads into upkeep but no use leads into a consumer good.", faults[1].Message); + Assert.Contains("'fuel' leads into Plate and upkeep but no use leads into a consumer good.", faults[1].Message); f.Good("pot", roles: new[] { ("Casing", 1) }); f.EditRecipe("commodity:fuel", r => r.Fills.Add("Casing")); Assert.Empty(EconomyChecks.ConsumerDemand(f.Cache)); } + [Fact] + public void ConsumerDemandFollowsUpkeepIntoAConsumerGoodAndIntoAFillerOfItsRole() + { + using var f = new EconomyFixture(); + f.Good("ore"); + f.Good("coolant", inputs: new[] { ("ore", 1) }); + Assert.Equal(new[] { "recipe:commodity:coolant" }, DemandRecords(f)); + + // The refrigerator is a consumer good (a commodity made from roles) that takes coolant as upkeep: coolant leads into it. + f.Good("fridge", roles: new[] { ("Casing", 1) }, upkeep: new[] { ("coolant", 1) }); + Assert.Empty(DemandRecords(f)); + + // Upkeep taken by a filler of a consumer role leads into that role the same way. + f.EditRecipe("commodity:fridge", r => r.Upkeep.Clear()); + Assert.Equal(new[] { "recipe:commodity:coolant" }, DemandRecords(f)); + f.Good("gasket", inputs: new[] { ("ore", 1) }, fills: new[] { "Casing" }, upkeep: new[] { ("coolant", 1) }); + Assert.Empty(DemandRecords(f)); + } + [Fact] public void ConsumerDemandCountsADrawnRoundAsOneUse() {