diff --git a/Satistools.Calculator.Test/CateriumProductsTest.cs b/Satistools.Calculator.Test/CateriumProductsTest.cs new file mode 100644 index 0000000..83192ad --- /dev/null +++ b/Satistools.Calculator.Test/CateriumProductsTest.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Satistools.Calculator.Graph; +using Satistools.Calculator.Test.SetUp; +using Xunit; +using Xunit.Abstractions; + +namespace Satistools.Calculator.Test; + +public class CateriumProductsTest : CalcTest +{ + public CateriumProductsTest(CalculatorFactory factory, ITestOutputHelper testOutputHelper) : base(factory, testOutputHelper) + { + } + + /// + /// Tests production of Caterium Ingot via alternate: Pure Caterium Ingot. + /// + [Fact] + public async Task Test_PureCateriumIngot() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_GoldIngot_C", 36f); + calculator.UseAlternateRecipe("Recipe_Alternate_PureCateriumIngot_C"); + ProductionGraph graph = await calculator.Calculate(); + + graph.Count.Should().Be(3); + + GraphNode ingot = graph["Desc_GoldIngot_C"]; + ingot.Product.TargetAmount.Should().Be(36f); + ingot.BuildingAmount.Should().Be(3f); + ingot.Building!.Id.Should().Be("Build_OilRefinery_C"); + ingot.NeededProducts.Should().HaveCount(2); + + GraphNode water = graph["Desc_Water_C"]; + water.Product.TargetAmount.Should().Be(72f); + + GraphNode ore = graph["Desc_OreGold_C"]; + ore.Product.TargetAmount.Should().Be(72f); + } +} \ No newline at end of file diff --git a/Satistools.Calculator.Test/IronProductsTest.cs b/Satistools.Calculator.Test/IronProductsTest.cs new file mode 100644 index 0000000..e515a90 --- /dev/null +++ b/Satistools.Calculator.Test/IronProductsTest.cs @@ -0,0 +1,173 @@ +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Satistools.Calculator.Graph; +using Satistools.Calculator.Test.SetUp; +using Xunit; +using Xunit.Abstractions; + +namespace Satistools.Calculator.Test; + +public class IronProductsTest : CalcTest +{ + public IronProductsTest(CalculatorFactory factory, ITestOutputHelper testOutputHelper) : base(factory, testOutputHelper) + { + } + + /// + /// Tests calculation of basic iron ingot production. + /// + [Fact] + public async Task Test_IronIngot() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronIngot_C", 30f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Count.Should().Be(2); + GraphNode topNode = graph.First(); + topNode.Product.TargetAmount.Should().Be(30f); + topNode.Product.Id.Should().Be("Desc_IronIngot_C"); + topNode.NeededProducts.Should().HaveCount(1); + topNode.UsedBy.Should().HaveCount(0); + + GraphNode lastNode = graph.Last(); + lastNode.Product.TargetAmount.Should().Be(30f); + lastNode.Product.Id.Should().Be("Desc_OreIron_C"); + lastNode.UsedBy.Should().HaveCount(1); + lastNode.NeededProducts.Should().HaveCount(0); + + NodeRelation oreIsUsed = lastNode.UsedBy.First(); + oreIsUsed.UnitsAmount.Should().Be(30f); + } + + /// + /// Test production of bigger amount of iron plate. + /// + [Fact] + public async Task Test_IronPlate() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronPlate_C", 50f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Count.Should().Be(3); + + GraphNode ingot = graph["Desc_IronIngot_C"]; + ingot.UsedBy.Should().HaveCount(1); + ingot.NeededProducts.Should().HaveCount(1); + ingot.BuildingAmount.Should().Be(2.5f); + + GraphNode plate = graph["Desc_IronPlate_C"]; + plate.NeededProducts.Should().HaveCount(1); + plate.BuildingAmount.Should().Be(2.5f); + } + + /// + /// Tests two target products. Iron Plate & Iron Rod. + /// + [Fact] + public async Task Test_DoubleProduction() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronPlate_C", 20f); + calculator.AddTargetProduct("Desc_IronRod_C", 15f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Count.Should().Be(4); + + GraphNode ore = graph["Desc_OreIron_C"]; + ore.Product.TargetAmount.Should().Be(45f); + + GraphNode ingot = graph["Desc_IronIngot_C"]; + ingot.Product.TargetAmount.Should().Be(45f); + ingot.UsedBy.Should().HaveCount(2); + } + + /// + /// Tests production of Reinforced Iron Plate. + /// + [Fact] + public async Task Test_ReinforcedIronPlate() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronPlateReinforced_C", 5f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(6); + + GraphNode ore = graph["Desc_OreIron_C"]; + ore.Product.TargetAmount.Should().Be(60f); + + GraphNode ingot = graph["Desc_IronIngot_C"]; + ingot.Product.TargetAmount.Should().Be(60f); + ingot.UsedBy.Should().HaveCount(2); + + GraphNode rip = graph["Desc_IronPlateReinforced_C"]; + rip.NeededProducts.Should().HaveCount(2); + } + + /// + /// Tests production of all advanced iron products - Rotors, Modular Frames & Reinforced Plates. + /// + [Fact] + public async Task Test_AdvancedIronProducts() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronPlateReinforced_C", 5f); + calculator.AddTargetProduct("Desc_Rotor_C", 4f); + calculator.AddTargetProduct("Desc_ModularFrame_C", 2f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(8); + + GraphNode ingot = graph["Desc_IronIngot_C"]; + ingot.Product.TargetAmount.Should().Be(153f); + ingot.UsedBy.Should().HaveCount(2); + + GraphNode rip = graph["Desc_IronPlateReinforced_C"]; + rip.Product.TargetAmount.Should().Be(8); + rip.NeededProducts.Should().HaveCount(2); + rip.UsedBy.Should().HaveCount(1); + } + + /// + /// Just like previous test, but every manufactuerer is producing at 100%. + /// + [Fact] + public async Task Test_BalancedProduction() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_IronPlateReinforced_C", 7f); + calculator.AddTargetProduct("Desc_Rotor_C", 4f); + calculator.AddTargetProduct("Desc_ModularFrame_C", 2f); + calculator.AddTargetProduct("Desc_IronScrew_C", 20f); + calculator.AddTargetProduct("Desc_IronRod_C", 13f); + calculator.AddTargetProduct("Desc_IronIngot_C", 15f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(8); + graph.Sum(n => n.BuildingAmount).Should().Be(27f); + } + + /// + /// Tests production of rotot via Alternate: Steel Rotor. + /// + [Fact] + public async Task Test_AlternateSteelRotor() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_Rotor_C", 5f); + calculator.UseAlternateRecipe("Recipe_Alternate_Rotor_C"); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(8); + + GraphNode ironOre = graph["Desc_OreIron_C"]; + ironOre.Product.TargetAmount.Should().Be(15f); + + GraphNode coal = graph["Desc_Coal_C"]; + coal.Product.TargetAmount.Should().Be(15f); + } +} \ No newline at end of file diff --git a/Satistools.Calculator.Test/OilProductsTest.cs b/Satistools.Calculator.Test/OilProductsTest.cs new file mode 100644 index 0000000..2b553c6 --- /dev/null +++ b/Satistools.Calculator.Test/OilProductsTest.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Satistools.Calculator.Graph; +using Satistools.Calculator.Test.SetUp; +using Xunit; +using Xunit.Abstractions; + +namespace Satistools.Calculator.Test; + +public class OilProductsTest : CalcTest +{ + public OilProductsTest(CalculatorFactory factory, ITestOutputHelper testOutputHelper) : base(factory, testOutputHelper) + { + } + + /// + /// Tests production of plastic. + /// + [Fact] + public async Task Test_PlasticProduction() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_Plastic_C", 20f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(2); + + GraphNode plastic = graph["Desc_Plastic_C"]; + plastic.Byproduct.Should().NotBeNull(); + plastic.Byproduct!.TargetAmount.Should().Be(10f); + + GraphNode residue = graph["Desc_HeavyOilResidue_C"]; // We are able to find the node also by byproduct + residue.ProductId.Should().Be(plastic.ProductId); + residue.ByproductId.Should().Be(residue.ByproductId); + } + + [Fact] + public async Task Test_PlasticAndFuel() + { + IProductionCalculator calculator = ServiceProvider.GetRequiredService(); + calculator.AddTargetProduct("Desc_Plastic_C", 20f); + calculator.AddTargetProduct("Desc_LiquidFuel_C", 6.66f); + ProductionGraph graph = await calculator.Calculate(); + + graph.Should().HaveCount(3); + } +} \ No newline at end of file diff --git a/Satistools.Calculator.Test/Satistools.Calculator.Test.csproj b/Satistools.Calculator.Test/Satistools.Calculator.Test.csproj new file mode 100644 index 0000000..381ac49 --- /dev/null +++ b/Satistools.Calculator.Test/Satistools.Calculator.Test.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + FactoryGame.json + PreserveNewest + + + + diff --git a/Satistools.Calculator.Test/SetUp/CalcTest.cs b/Satistools.Calculator.Test/SetUp/CalcTest.cs new file mode 100644 index 0000000..80862bb --- /dev/null +++ b/Satistools.Calculator.Test/SetUp/CalcTest.cs @@ -0,0 +1,12 @@ +using Sagittaras.Model.TestFramework; +using Satistools.GameData; +using Xunit.Abstractions; + +namespace Satistools.Calculator.Test.SetUp; + +public abstract class CalcTest : UnitTest +{ + protected CalcTest(CalculatorFactory factory, ITestOutputHelper testOutputHelper) : base(factory, testOutputHelper) + { + } +} \ No newline at end of file diff --git a/Satistools.Calculator.Test/SetUp/CalculatorFactory.cs b/Satistools.Calculator.Test/SetUp/CalculatorFactory.cs new file mode 100644 index 0000000..245e9e4 --- /dev/null +++ b/Satistools.Calculator.Test/SetUp/CalculatorFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Sagittaras.Model.TestFramework; +using Sagittaras.Repository.Extensions; +using Satistools.Calculator.Extensions; +using Satistools.GameData; +using Satistools.GameData.Items; +using Satistools.GameData.Recipes; + +namespace Satistools.Calculator.Test.SetUp; + +public class CalculatorFactory : TestFactory +{ + protected override void OnConfiguring(ServiceCollection services) + { + services.AddDbContext(options => + { + options.UseInMemoryDatabase(GetConnectionString(Engine.InMemory)); + }); + services.UseRepositoryPattern(options => + { + options.AddRepository(); + options.AddRepository(); + }); + services.AddCalculator(); + } +} \ No newline at end of file diff --git a/Satistools.Calculator/AnalyseResult.cs b/Satistools.Calculator/AnalyseResult.cs new file mode 100644 index 0000000..c75e4d8 --- /dev/null +++ b/Satistools.Calculator/AnalyseResult.cs @@ -0,0 +1,23 @@ +using Satistools.Calculator.Graph; + +namespace Satistools.Calculator; + +public readonly struct AnalyseResult +{ + public GraphNode ProductNode { get; } + public float ProductAmount { get; } + public IEnumerable Byproducts { get; } = Array.Empty(); + + public AnalyseResult(GraphNode productNode, float productAmount) + { + ProductNode = productNode; + ProductAmount = productAmount; + } + + public AnalyseResult(GraphNode productNode, float productAmount, IEnumerable byproducts) + { + ProductNode = productNode; + ProductAmount = productAmount; + Byproducts = byproducts; + } +} \ No newline at end of file diff --git a/Satistools.Calculator/Extensions/ServiceExtension.cs b/Satistools.Calculator/Extensions/ServiceExtension.cs new file mode 100644 index 0000000..9c0097c --- /dev/null +++ b/Satistools.Calculator/Extensions/ServiceExtension.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Satistools.Calculator.Extensions; + +public static class ServiceExtension +{ + public static void AddCalculator(this IServiceCollection services) + { + services.AddTransient(); + } +} \ No newline at end of file diff --git a/Satistools.Calculator/Graph/GraphNode.cs b/Satistools.Calculator/Graph/GraphNode.cs new file mode 100644 index 0000000..f431a98 --- /dev/null +++ b/Satistools.Calculator/Graph/GraphNode.cs @@ -0,0 +1,119 @@ +using Satistools.GameData.Buildings; +using Satistools.GameData.Items; + +namespace Satistools.Calculator.Graph; + +/// +/// A single node of network graph for production. +/// +public class GraphNode +{ + /// + /// Initializes a new node in production graph. + /// + /// Building needed for the production of selected item. + /// How many buildings are needed to produce target amount. + /// Item which is being produced by this node. + /// How units of the product is needed per minute. + public GraphNode(Building? building, float buildingAmount, Item product, float productAmount) + { + Building = building; + BuildingAmount = buildingAmount; + + Product = new NodeProduct(product, productAmount); + } + + public GraphNode(Building? building, float buildingAmount, Item product, float productAmount, Item byproduct, float byproductAmount) + { + Building = building; + BuildingAmount = buildingAmount; + + Product = new NodeProduct(product, productAmount); + Byproduct = new NodeProduct(byproduct, byproductAmount); + } + + /// + /// ID of item which is produced as the main product. + /// + public string ProductId => Product.Item.Id; + + /// + /// ID of byproduct if exists. + /// + public string ByproductId => Byproduct?.Item.Id ?? string.Empty; + + /// + /// In which building is the product manufactured. + /// + public Building? Building { get; } + + /// + /// How many buildings are required. + /// + public float BuildingAmount { get; set; } + + /// + /// Informations about item produced by this node. + /// + public NodeProduct Product { get; } + + /// + /// Informations about byproduct if it's produced by the recipe. + /// + public NodeProduct? Byproduct { get; } + + /// + /// List of all nodes which are using the selected product. + /// + public ICollection UsedBy { get; } = new List(); + + /// + /// List of all nodes which are used for the product of this one. + /// + public ICollection NeededProducts { get; } = new List(); + + /// + /// Checks if the node is producing the item. + /// + /// Item identification. + /// True if the item is produced as product or byproduct. + public bool Produces(string itemId) + { + return ProductId == itemId || ByproductId == itemId; + } + + public void UpdateUsage(NodeRelation nodeRelation) + { + NodeRelation? existing = UsedBy.SingleOrDefault(u => u.TargetNode.ProductId == nodeRelation.TargetNode.ProductId); + if (existing is not null) + { + existing.UnitsAmount += nodeRelation.UnitsAmount; + return; + } + + UsedBy.Add(nodeRelation); + } + + public void UpdateNeeds(NodeRelation nodeRelation) + { + NodeRelation? existing = NeededProducts.SingleOrDefault(u => u.TargetNode.ProductId == nodeRelation.TargetNode.ProductId); + if (existing is not null) + { + existing.UnitsAmount += nodeRelation.UnitsAmount; + return; + } + + NeededProducts.Add(nodeRelation); + } + + public override string ToString() + { + string description = $"{Product.Item.DisplayName} {Product.TargetAmount}/min ({Product.UsedAmount} used) ({Product.PercentageUsage}%)"; + if (Building is not null) + { + description += $"; {BuildingAmount}x {Building.DisplayName}"; + } + + return description; + } +} \ No newline at end of file diff --git a/Satistools.Calculator/Graph/NodeProduct.cs b/Satistools.Calculator/Graph/NodeProduct.cs new file mode 100644 index 0000000..93c0664 --- /dev/null +++ b/Satistools.Calculator/Graph/NodeProduct.cs @@ -0,0 +1,35 @@ +using Satistools.GameData.Items; + +namespace Satistools.Calculator.Graph; + +public class NodeProduct +{ + public NodeProduct(Item item, float targetAmount) + { + Item = item; + TargetAmount = targetAmount; + UsedAmount = 0; + } + + public string Id => Item.Id; + + /// + /// Which item is being produced by this node. + /// + public Item Item { get; } + + /// + /// How many parts of the product are being produced by minute. + /// + public float TargetAmount { get; set; } + + /// + /// How many units of the product is used. + /// + public float UsedAmount { get; set; } + + /// + /// How many percents of the production is used. + /// + public float PercentageUsage => UsedAmount / TargetAmount * 100; +} \ No newline at end of file diff --git a/Satistools.Calculator/Graph/NodeRelation.cs b/Satistools.Calculator/Graph/NodeRelation.cs new file mode 100644 index 0000000..b6a788d --- /dev/null +++ b/Satistools.Calculator/Graph/NodeRelation.cs @@ -0,0 +1,35 @@ +using Satistools.GameData.Items; + +namespace Satistools.Calculator.Graph; + +/// +/// Represents a single relation between nodes in both directions. +/// +public class NodeRelation +{ + /// + /// Initializes a new relation between two nodes. + /// + /// The target node of the relation. + /// How many units is required in the relation. + public NodeRelation(GraphNode targetNode, float unitsAmount) + { + TargetNode = targetNode; + UnitsAmount = unitsAmount; + } + + /// + /// Reference to the other node. + /// + public GraphNode TargetNode { get; } + + /// + /// How many units of the product are need by the target node. + /// + public float UnitsAmount { get; set; } + + public override string ToString() + { + return $"{TargetNode.Product.Item.DisplayName} {UnitsAmount} units/min"; + } +} \ No newline at end of file diff --git a/Satistools.Calculator/Graph/ProductionGraph.cs b/Satistools.Calculator/Graph/ProductionGraph.cs new file mode 100644 index 0000000..22f288d --- /dev/null +++ b/Satistools.Calculator/Graph/ProductionGraph.cs @@ -0,0 +1,87 @@ +using System.Collections; + +namespace Satistools.Calculator.Graph; + +/// +/// A collection of all nodes representing network graph describing how to produce selected items. +/// +public class ProductionGraph : IEnumerable +{ + /// + /// List of all nodes inside the graph. + /// + private readonly Dictionary _nodes = new(); + + /// + /// Gets node by the ID of item which is producing. + /// + /// Identification of the produced item by node. + public GraphNode this[string id] + { + get { return _nodes.Values.Single(n => n.Produces(id)); } + } + + /// + /// How many nodes are already in the graph. + /// + public int Count => _nodes.Count; + + /// + /// Adds new node without any relation. Or update the existing one. + /// + /// Instance of new node. + public GraphNode AddOrUpdate(GraphNode node) + { + if (_nodes.ContainsKey(node.ProductId)) + { + GraphNode existing = _nodes[node.ProductId]; + existing.BuildingAmount += node.BuildingAmount; + + existing.Product.TargetAmount += node.Product.TargetAmount; + if (node.Byproduct is not null) + { + existing.Byproduct!.TargetAmount += node.Byproduct.TargetAmount; + } + + return existing; + } + + _nodes.Add(node.ProductId, node); + return node; + } + + /// + /// Creates a relation between two nodes. + /// + /// Instance of node which is used by the product. + /// + /// Amount of product parts used in this relation. + public void NodeIsUsedBy(GraphNode node, NodeProduct product, float amount) + { + // node is like Iron Ore & neededNode is like Iron Ingot + GraphNode neededNode = this[product.Id]; + neededNode.UpdateNeeds(new NodeRelation(node, amount)); + node.UpdateUsage(new NodeRelation(neededNode, amount)); + + node.Product.UsedAmount += amount; + } + + /*public void NodeNeedsProduct(GraphNode node, string id, float amount) + { + GraphNode usedNode = this[id]; + usedNode.UpdateUsage(new NodeRelation(node, amount)); + node.UpdateNeeds(new NodeRelation(usedNode, amount)); + }*/ + + /// + public IEnumerator GetEnumerator() + { + return _nodes.Values.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/Satistools.Calculator/IProductionCalculator.cs b/Satistools.Calculator/IProductionCalculator.cs new file mode 100644 index 0000000..aa47c80 --- /dev/null +++ b/Satistools.Calculator/IProductionCalculator.cs @@ -0,0 +1,25 @@ +using Satistools.Calculator.Graph; + +namespace Satistools.Calculator; + +public interface IProductionCalculator +{ + /// + /// Adds a new final product to the calculator. + /// + /// ID of item which will be final. + /// How many parts of the product should be produced. + void AddTargetProduct(string itemId, float amount); + + /// + /// Adds a new alternate recipe for calculations. + /// + /// Identification of the recipe. + void UseAlternateRecipe(string recipeId); + + /// + /// Calculates the production graph for the set inputs. + /// + /// Returns an instance of calculated graph of production. + Task Calculate(); +} \ No newline at end of file diff --git a/Satistools.Calculator/ProductionCalculator.cs b/Satistools.Calculator/ProductionCalculator.cs new file mode 100644 index 0000000..3fff1d0 --- /dev/null +++ b/Satistools.Calculator/ProductionCalculator.cs @@ -0,0 +1,136 @@ +using Satistools.Calculator.Graph; +using Satistools.GameData.Items; +using Satistools.GameData.Recipes; + +namespace Satistools.Calculator; + +/// +/// Calculates graph for production of needed products. +/// +public class ProductionCalculator : IProductionCalculator +{ + private readonly IRecipeRepository _recipeRepository; + private readonly IItemRepository _itemRepository; + + /// + /// Contains IDs of all items which production should calculated in Tuple with target amount. + /// + private readonly List<(string, float)> _targetIds = new(); + + /// + /// Contains IDs of used alternate recipes for calculations. + /// + private readonly List _alternateIds = new(); + + /// + /// Contains all found alternate recipes. + /// + private readonly List _alternateRecipes = new(); + + public ProductionCalculator(IRecipeRepository recipeRepository, IItemRepository itemRepository) + { + _recipeRepository = recipeRepository; + _itemRepository = itemRepository; + } + + /// + public void AddTargetProduct(string itemId, float amount) + { + _targetIds.Add((itemId, amount)); + } + + /// + public void UseAlternateRecipe(string recipeId) + { + _alternateIds.Add(recipeId); + } + + /// + public async Task Calculate() + { + if (_alternateIds.Count > 0) + { + _alternateRecipes.AddRange(await _recipeRepository.FindByIds(_alternateIds)); + } + + ProductionGraph graph = new(); + + foreach ((string targetId, float targetAmount) in _targetIds) + { + Item? item = await _itemRepository.Get(targetId); + if (item is null) + { + throw new Exception($"Target item {targetId} was not found"); + } + + Recipe? recipe = await GetRecipe(targetId); + if (recipe is null) + { + throw new Exception($"Recipe for {item.Id} was not found"); + } + + RecipeProduct product = recipe.GetProduct(targetId); + float productionRate = targetAmount / product.AmountPerMin; + await AnalyseRecipePart(graph, product, productionRate); + } + + return graph; + } + + private async Task AnalyseRecipePart(ProductionGraph graph, RecipePart part, float productionRate) + { + float amount; + GraphNode node; + + Recipe? recipe = await GetRecipe(part.ItemId); + if (recipe is null) + { + // Recipe is not available, thus this item must be resource. + amount = part.AmountPerMin * productionRate; + node = new GraphNode(null, 0, part.Item, amount); + return new AnalyseResult(graph.AddOrUpdate(node), amount); + } + + RecipeProduct product = recipe.GetProduct(part.ItemId); + amount = part.AmountPerMin * productionRate; + float buildingsCount = amount / product.AmountPerMin; // Serves also as the production rate for the next ingredient. + + RecipeProduct? byproduct = recipe.Products.FirstOrDefault(p => p.ItemId != part.ItemId); + + node = byproduct is null + ? new GraphNode(recipe.ProducedIn, buildingsCount, part.Item, amount) + : new GraphNode(recipe.ProducedIn, buildingsCount, part.Item, amount, byproduct.Item, byproduct.AmountPerMin * productionRate); + + node = graph.AddOrUpdate(node); + + foreach (RecipeIngredient ingredient in recipe.Ingredients) + { + AnalyseResult result = await AnalyseRecipePart(graph, ingredient, buildingsCount); + graph.NodeIsUsedBy(result.ProductNode, node.Product, result.ProductAmount); + } + + return new AnalyseResult(node, amount); + } + + /// + /// Gets recipe for the product. + /// + /// + /// First searches in the list of used alternate recipes, then queries the database for the original one. + /// + /// Identification of product. + /// Found recipe or null if the product does not have any recipe. + private async Task GetRecipe(string productId) + { + if (_alternateRecipes.Count > 0) + { + Recipe? alternate = _alternateRecipes.Find(r => r.Products.Any(p => p.ItemId == productId)); + if (alternate is not null) + { + return alternate; + } + } + + return await _recipeRepository.GetOriginalRecipe(productId); + } +} \ No newline at end of file diff --git a/Satistools.Model.Repository/Satistools.Model.Repository.csproj b/Satistools.Calculator/Satistools.Calculator.csproj similarity index 74% rename from Satistools.Model.Repository/Satistools.Model.Repository.csproj rename to Satistools.Calculator/Satistools.Calculator.csproj index c4d18ef..7506567 100644 --- a/Satistools.Model.Repository/Satistools.Model.Repository.csproj +++ b/Satistools.Calculator/Satistools.Calculator.csproj @@ -7,7 +7,7 @@ - + diff --git a/Satistools.GameData.Test/Satistools.GameData.Test.csproj b/Satistools.GameData.Test/Satistools.GameData.Test.csproj index 3177df8..67d546e 100644 --- a/Satistools.GameData.Test/Satistools.GameData.Test.csproj +++ b/Satistools.GameData.Test/Satistools.GameData.Test.csproj @@ -9,7 +9,9 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,7 +25,6 @@ - diff --git a/Satistools.GameData.Test/SetUp/GameDataFactory.cs b/Satistools.GameData.Test/SetUp/GameDataFactory.cs index f6b9e44..9ceb154 100644 --- a/Satistools.GameData.Test/SetUp/GameDataFactory.cs +++ b/Satistools.GameData.Test/SetUp/GameDataFactory.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Satistools.ModelTest; +using Sagittaras.Model.TestFramework; namespace Satistools.GameData.Test.SetUp; @@ -10,7 +10,7 @@ protected override void OnConfiguring(ServiceCollection services) { services.AddDbContext(options => { - options.UseInMemoryDatabase(GetDatabaseName()); + options.UseInMemoryDatabase(GetConnectionString(Engine.InMemory)); }); } } \ No newline at end of file diff --git a/Satistools.GameData.Test/SetUp/GameDataTest.cs b/Satistools.GameData.Test/SetUp/GameDataTest.cs index 35437cb..f6a5c26 100644 --- a/Satistools.GameData.Test/SetUp/GameDataTest.cs +++ b/Satistools.GameData.Test/SetUp/GameDataTest.cs @@ -1,4 +1,4 @@ -using Satistools.ModelTest; +using Sagittaras.Model.TestFramework; using Xunit.Abstractions; namespace Satistools.GameData.Test.SetUp; @@ -6,7 +6,7 @@ namespace Satistools.GameData.Test.SetUp; /// /// TestFixture implementation for tests of GameData. /// -public abstract class GameDataTest : TestFixture +public abstract class GameDataTest : UnitTest { protected GameDataTest(GameDataFactory factory, ITestOutputHelper testOutputHelper) : base(factory, testOutputHelper) { diff --git a/Satistools.GameData/Extensions/ServiceExtension.cs b/Satistools.GameData/Extensions/ServiceExtension.cs index e9cebeb..257d57d 100644 --- a/Satistools.GameData/Extensions/ServiceExtension.cs +++ b/Satistools.GameData/Extensions/ServiceExtension.cs @@ -1,10 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Sagittaras.Repository.Extensions; using Satistools.GameData.Items; using Satistools.GameData.Recipes; -using Satistools.Model.Repository; -using Satistools.Model.Repository.Extensions; namespace Satistools.GameData.Extensions; @@ -21,7 +20,6 @@ public static void AddGameDataModel(this IServiceCollection services, IConfigura { options.UseSqlite(configuration.GetConnectionString("sqlite")); }); - services.AddScoped(b => b.GetRequiredService()); services.UseRepositoryPattern(options => { options.AddRepository(); diff --git a/Satistools.GameData/GameDataContext.cs b/Satistools.GameData/GameDataContext.cs index 1c3141d..f252a2d 100644 --- a/Satistools.GameData/GameDataContext.cs +++ b/Satistools.GameData/GameDataContext.cs @@ -9,11 +9,10 @@ using Satistools.GameData.Items; using Satistools.GameData.Recipes; using Satistools.GameData.Recipes.Mappers; -using Satistools.Model.Repository; namespace Satistools.GameData; -public class GameDataContext : RepositoryContext +public class GameDataContext : DbContext { /// /// If the context is already preconfigured, the database is not populated with game date. @@ -37,9 +36,8 @@ public GameDataContext() public GameDataContext( DbContextOptions options, - IConfiguration configuration, - IEnumerable repositories - ) : base(options, repositories) + IConfiguration configuration + ) : base(options) { _isDevelopment = _populateData = configuration["ASPNETCORE_ENVIRONMENT"] == "Development"; } @@ -70,7 +68,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) return; } - FactoryGameReader reader = new(Path.Combine(Directory.GetCurrentDirectory(), ".."), "FactoryGame.json"); + FactoryGameReader reader = new(Directory.GetCurrentDirectory(), "FactoryGame.json"); List items = ImportItems(modelBuilder, reader); IEnumerable buildings = ImportBuildings(modelBuilder, reader); ImportRecipes(modelBuilder, reader, buildings, items); diff --git a/Satistools.GameData/Items/IItemRepository.cs b/Satistools.GameData/Items/IItemRepository.cs index 76c73c2..8a3007f 100644 --- a/Satistools.GameData/Items/IItemRepository.cs +++ b/Satistools.GameData/Items/IItemRepository.cs @@ -1,4 +1,4 @@ -using Satistools.Model.Repository; +using Sagittaras.Repository; namespace Satistools.GameData.Items; diff --git a/Satistools.GameData/Items/ItemRepository.cs b/Satistools.GameData/Items/ItemRepository.cs index bfb7864..307129c 100644 --- a/Satistools.GameData/Items/ItemRepository.cs +++ b/Satistools.GameData/Items/ItemRepository.cs @@ -1,12 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Sagittaras.Repository; using Satistools.DataReader.Entities.Items; -using Satistools.Model.Repository; namespace Satistools.GameData.Items; public class ItemRepository : Repository, IItemRepository { - public ItemRepository(RepositoryContext dbContext) : base(dbContext) + public ItemRepository(DbContext dbContext) : base(dbContext) { } diff --git a/Satistools.GameData/Recipes/IRecipeRepository.cs b/Satistools.GameData/Recipes/IRecipeRepository.cs index 6955f3c..df30fc2 100644 --- a/Satistools.GameData/Recipes/IRecipeRepository.cs +++ b/Satistools.GameData/Recipes/IRecipeRepository.cs @@ -1,9 +1,26 @@ -using Satistools.Model.Repository; +using Sagittaras.Repository; namespace Satistools.GameData.Recipes; public interface IRecipeRepository : IRepository { + /// + /// Gets the original which is used for production of the item. + /// + /// + /// By original recipe is meant the one, which is not marked as alternate. + /// + /// ID of item used for production of the item. + /// Instance of original recipe for the item or null, if there is no original recipe. + Task GetOriginalRecipe(string itemId); + + /// + /// Search recipes by their IDs. + /// + /// Enumerable of all IDs which should be found. + /// Enumeration of all found recipes. + Task> FindByIds(IEnumerable recipeIds); + /// /// Finds all recipes which are producing selected item. /// diff --git a/Satistools.GameData/Recipes/Mappers/RecipeMapper.cs b/Satistools.GameData/Recipes/Mappers/RecipeMapper.cs index b26a726..ba40405 100644 --- a/Satistools.GameData/Recipes/Mappers/RecipeMapper.cs +++ b/Satistools.GameData/Recipes/Mappers/RecipeMapper.cs @@ -9,6 +9,18 @@ namespace Satistools.GameData.Recipes.Mappers; /// public static class RecipeMapper { + /// + /// List of recipes which is alternate to the original one, yet they don't need to be unlocked via HDD + /// and are not by default marked as alternate. + /// + private static readonly List OriginalAlternatives = new() + { + "Recipe_ResidualPlastic_C", + "Recipe_ResidualRubber_C", + "Recipe_LiquidFuel_C", + "Recipe_UnpackageFuel_C" // TODO: All unpackaging recipes should not be default choice + }; + public static IMapper Create(IEnumerable buildings) { return new MapperConfiguration(cfg => @@ -22,7 +34,13 @@ public static IMapper Create(IEnumerable buildings) .ForMember(d => d.Products, opt => opt.Ignore()) .ForMember(d => d.ProducedInId, opt => opt.MapFrom(src => src.ProducedIn.Where(p => buildings.Any(b => b.ClassName == p.ClassName)).Select(p => p.ClassName).Single())) .ForMember(d => d.ProducedIn, opt => opt.Ignore()) - .ForMember(d => d.IsAlternate, opt => opt.MapFrom(src => src.ClassName.Contains("_Alternate_"))); + .ForMember(d => d.IsAlternate, opt => opt.MapFrom(src => src.ClassName.Contains("_Alternate_"))) + .ForMember(d => d.IsDefault, opt => opt.MapFrom(src => IsRecipeDefault(src))); }).CreateMapper(); } + + private static bool IsRecipeDefault(RecipeDescriptor descriptor) + { + return !descriptor.ClassName.Contains("_Alternate_") && OriginalAlternatives.All(d => d != descriptor.ClassName); + } } \ No newline at end of file diff --git a/Satistools.GameData/Recipes/Recipe.cs b/Satistools.GameData/Recipes/Recipe.cs index a5c8550..4533e0c 100644 --- a/Satistools.GameData/Recipes/Recipe.cs +++ b/Satistools.GameData/Recipes/Recipe.cs @@ -36,6 +36,11 @@ public class Recipe /// Marks if the recipe is alternative. /// public bool IsAlternate { get; set; } + + /// + /// Marks if the recipe is default choice when there is multiple alternative unlocked by default. + /// + public bool IsDefault { get; set; } /// /// @@ -44,4 +49,14 @@ public class Recipe public ICollection Ingredients { get; set; } = null!; public ICollection Products { get; set; } = null!; + + /// + /// Gets the recipe product by ID of item. + /// + /// Item identification. + /// Found recipe product. + public RecipeProduct GetProduct(string id) + { + return Products.Single(p => p.ItemId == id); + } } \ No newline at end of file diff --git a/Satistools.GameData/Recipes/RecipeRepository.cs b/Satistools.GameData/Recipes/RecipeRepository.cs index ff33e8c..7799dbb 100644 --- a/Satistools.GameData/Recipes/RecipeRepository.cs +++ b/Satistools.GameData/Recipes/RecipeRepository.cs @@ -1,11 +1,12 @@ using Microsoft.EntityFrameworkCore; -using Satistools.Model.Repository; +using Sagittaras.Repository; +using Satistools.DataReader.Entities.Items; namespace Satistools.GameData.Recipes; public class RecipeRepository : Repository, IRecipeRepository { - public RecipeRepository(RepositoryContext dbContext) : base(dbContext) + public RecipeRepository(DbContext dbContext) : base(dbContext) { } @@ -20,18 +21,22 @@ public RecipeRepository(RepositoryContext dbContext) : base(dbContext) .AsSplitQuery(); /// - public async Task> FindOrderedByName() + public async Task GetOriginalRecipe(string itemId) { - return await FullInfoSource - .OrderBy(r => r.DisplayName) - .ToListAsync(); + return await FullInfoSource.SingleOrDefaultAsync(r => r.IsDefault && !r.IsAlternate && r.Products.Any(p => p.ItemId == itemId && p.Item.ItemCategory != ItemCategory.Resource)); + } + + /// + public async Task> FindByIds(IEnumerable recipeIds) + { + return await FullInfoSource.Where(r => recipeIds.Contains(r.Id)).ToListAsync(); } /// public async Task> FindRecipesProducingItem(string itemId) { return await (from recipe in FullInfoSource - join product in JoinDbSet() on recipe.Id equals product.RecipeId + join product in Join() on recipe.Id equals product.RecipeId where product.ItemId == itemId select recipe).ToListAsync(); } @@ -40,7 +45,7 @@ join product in JoinDbSet() on recipe.Id equals product.RecipeId public async Task> FindRecipesUsingItem(string itemId) { return await (from recipe in FullInfoSource - join ingredient in JoinDbSet() on recipe.Id equals ingredient.RecipeId + join ingredient in Join() on recipe.Id equals ingredient.RecipeId where ingredient.ItemId == itemId select recipe).ToListAsync(); } diff --git a/Satistools.GameData/Satistools.GameData.csproj b/Satistools.GameData/Satistools.GameData.csproj index 08fbc23..24b94c8 100644 --- a/Satistools.GameData/Satistools.GameData.csproj +++ b/Satistools.GameData/Satistools.GameData.csproj @@ -14,6 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -25,7 +26,6 @@ - diff --git a/Satistools.Model.Repository/Extensions/ServiceExtension.cs b/Satistools.Model.Repository/Extensions/ServiceExtension.cs deleted file mode 100644 index e88119a..0000000 --- a/Satistools.Model.Repository/Extensions/ServiceExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Satistools.Model.Repository.Extensions -{ - /// - /// - /// - public static class ServiceExtension - { - /// - /// Enables usage of repository pattern with the data model. - /// - /// - /// - public static void UseRepositoryPattern(this IServiceCollection services, Action? options) - { - options?.Invoke(new RepositoryPatternBuilderOptions(services)); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/IRepository.cs b/Satistools.Model.Repository/IRepository.cs deleted file mode 100644 index dbb30ce..0000000 --- a/Satistools.Model.Repository/IRepository.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository; - -/// -/// Basic non-geric repository definition. -/// -public interface IRepository -{ - /// - /// The type of entity used with this repository. - /// - Type EntityType { get; } - - /// - /// Gets whether this repository has unsaved changes. - /// - bool HasChanges { get; } - - /// - /// Saves all changes made to this repository. - /// - /// How many rows was saved to the database from this repository. - void SaveChanges(); - - /// - /// Saves all changes made to this repository as async task. - /// - /// How many rows was saved to the database from this repository. - Task SaveChangesAsync(); -} - -/// -/// Generic repository controlling the type of used entity. -/// -/// The data of entity used with repository. -public interface IRepository : IRepository where TEntity : class -{ - /// - /// Access to the DbSet of repository for entity. - /// - DbSet Table { get; } - - /// - /// Gets all entities in this repository. - /// - /// An enumerable of all entites. - Task> GetAll(); - - /// - /// Inserts a new record to the repository. - /// - /// Entity to be saved list. - void Insert(TEntity entity); - - /// - /// Inserts a new range of entites to the repisitory. - /// - /// Enumerable of entities to be saved. - void InsertRange(IEnumerable entities); - - /// - /// Updates a entity in repository. - /// - /// Entity to be updated. - void Update(TEntity entity); - - /// - /// Updates a range of entites. - /// - /// Enumerable of entities to be updated. - void UpdateRange(IEnumerable entities); - - /// - /// Removes an entity from repository. - /// - /// Entity to be removed. - void Remove(TEntity entity); - - /// - /// Removes a range of entities from repository. - /// - /// Enumerable of netities to be removed. - void RemoveRange(IEnumerable entities); -} - -/// -/// Generic repository expanding posibilities of query by the generic type of primary key. -/// -/// The datatype of saved entity. -/// The datatype of primary key on entity. -public interface IRepository : IRepository where TEntity : class -{ - /// - /// Gets an entity by the primary key value. - /// - /// Value of primary key. - /// Awaitable task resulting in entity or null if not found. - Task Get(TKey id); -} - -/// -/// Repository supporting composite key identification. -/// -/// The type of used entity. -/// The type of first part of primary key. -/// The type of second part of primary key. -public interface IRepository : IRepository where TEntity : class -{ - /// - /// Gets the entity by the both primary key types. - /// - /// Value of first part of PK. - /// Value of second part of PK. - /// Entity if found or null. - Task Get(TFirstKey firstKey, TSecondKey secondKey); -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/IRepositoryOperation.cs b/Satistools.Model.Repository/Operations/IRepositoryOperation.cs deleted file mode 100644 index 97475ad..0000000 --- a/Satistools.Model.Repository/Operations/IRepositoryOperation.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Satistools.Model.Repository.Operations -{ - - /// - /// Represents an postponed operation which whould be applied when the repository is saving its changes. - /// - public interface IRepositoryOperation - { - /// - /// Apply the changes made by this operation. - /// - void Apply(); - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/InsertOperation.cs b/Satistools.Model.Repository/Operations/InsertOperation.cs deleted file mode 100644 index 6e6de47..0000000 --- a/Satistools.Model.Repository/Operations/InsertOperation.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Represents an insert operation. - /// - public class InsertOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _dbContext; - private readonly TEntity _entity; - - /// - /// - /// - /// Accessed context by repository. - /// Entity to be saved. - public InsertOperation(DbContext dbContext, TEntity entity) - { - _dbContext = dbContext; - _entity = entity; - } - - /// - public void Apply() - { - _dbContext.Add(_entity); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/InsertRangeOperation.cs b/Satistools.Model.Repository/Operations/InsertRangeOperation.cs deleted file mode 100644 index af3e931..0000000 --- a/Satistools.Model.Repository/Operations/InsertRangeOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Operation inserting a range of entities. - /// - /// Used entity. - public class InsertRangeOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _context; - private readonly IEnumerable _entities; - - /// - /// - /// - /// - /// - public InsertRangeOperation(DbContext context, IEnumerable entities) - { - _context = context; - _entities = entities; - } - - /// - public void Apply() - { - _context.AddRange(_entities); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/RemoveOperation.cs b/Satistools.Model.Repository/Operations/RemoveOperation.cs deleted file mode 100644 index 90b1eea..0000000 --- a/Satistools.Model.Repository/Operations/RemoveOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Operation removing entity from database. - /// - /// Type of used entity. - public class RemoveOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _context; - private readonly TEntity _entity; - - /// - /// - /// - /// - /// - public RemoveOperation(DbContext context, TEntity entity) - { - _context = context; - _entity = entity; - } - - /// - public void Apply() - { - _context.Remove(_entity); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/RemoveRangeOperation.cs b/Satistools.Model.Repository/Operations/RemoveRangeOperation.cs deleted file mode 100644 index 151b991..0000000 --- a/Satistools.Model.Repository/Operations/RemoveRangeOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Operation removing set of entities from database. - /// - /// Type of used entities. - public class RemoveRangeOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _context; - private readonly IEnumerable _entities; - - /// - /// - /// - /// - /// - public RemoveRangeOperation(DbContext context, IEnumerable entities) - { - _context = context; - _entities = entities; - } - - /// - public void Apply() - { - _context.RemoveRange(_entities); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/UpdateOperation.cs b/Satistools.Model.Repository/Operations/UpdateOperation.cs deleted file mode 100644 index 17f880c..0000000 --- a/Satistools.Model.Repository/Operations/UpdateOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Operation updating an entity in database. - /// - /// Type of used entity. - public class UpdateOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _context; - private readonly TEntity _entity; - - /// - /// - /// - /// - /// - public UpdateOperation(DbContext context, TEntity entity) - { - _context = context; - _entity = entity; - } - - /// - public void Apply() - { - _context.Update(_entity); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Operations/UpdateRangeOperation.cs b/Satistools.Model.Repository/Operations/UpdateRangeOperation.cs deleted file mode 100644 index c8374db..0000000 --- a/Satistools.Model.Repository/Operations/UpdateRangeOperation.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository.Operations -{ - /// - /// Operation updating a range of entities in database. - /// - /// Type of used entity. - public class UpdateRangeOperation : IRepositoryOperation where TEntity : class - { - private readonly DbContext _context; - private readonly IEnumerable _entities; - - /// - /// - /// - /// - /// - public UpdateRangeOperation(DbContext context, IEnumerable entities) - { - _context = context; - _entities = entities; - } - - /// - public void Apply() - { - _context.UpdateRange(_entities); - } - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/Repository.cs b/Satistools.Model.Repository/Repository.cs deleted file mode 100644 index d20b99a..0000000 --- a/Satistools.Model.Repository/Repository.cs +++ /dev/null @@ -1,191 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Satistools.Model.Repository.Operations; - -namespace Satistools.Model.Repository; - - /// - /// Internal implementation of repository. - /// - /// - /// Repository without its generic type is useless. - /// - public abstract class Repository : IRepository - { - /// - /// - /// - /// - /// - protected Repository(RepositoryContext dbContext, Type entityType) - { - Context = dbContext; - EntityType = entityType; - } - - /// - public Type EntityType { get; } - - /// - public bool HasChanges => Operations.Count > 0; - - /// - /// Protected access to the context of database the repository is working with. - /// - internal RepositoryContext Context { get; } - - /// - /// Queue of operations which should be executed the repository is saving changes. - /// - internal Queue Operations { get; } = new(); - - /// - public void SaveChanges() - { - SaveChangesAsync().Wait(); - } - - /// - public async Task SaveChangesAsync() - { - while (Operations.TryDequeue(out IRepositoryOperation? operation)) - { - operation.Apply(); - } - - await Context.SaveChangesAsync(); - } - } - - /// - /// Generic implementation of repository. - /// - /// Target type of entity the repository is working with. - public abstract class Repository : Repository, IRepository where TEntity : class - { - /// - /// - /// - /// - protected Repository(RepositoryContext dbContext) : base(dbContext, typeof(TEntity)) - { - Table = dbContext.Set(); - Queryable = Table.AsQueryable(); - } - - /// - /// Original for entity. - /// - public DbSet Table { get; } - - /// - /// Queryable data source of - /// - protected IQueryable Queryable { get; } - - /// - public async Task> GetAll() - { - return await Queryable.ToListAsync(); - } - - /// - public void Insert(TEntity entity) - { - Operations.Enqueue(new InsertOperation(Context, entity)); - } - - /// - public void InsertRange(IEnumerable entities) - { - Operations.Enqueue(new InsertRangeOperation(Context, entities)); - } - - /// - public void Update(TEntity entity) - { - Operations.Enqueue(new UpdateOperation(Context, entity)); - } - - /// - public void UpdateRange(IEnumerable entities) - { - Operations.Enqueue(new UpdateRangeOperation(Context, entities)); - } - - /// - public void Remove(TEntity entity) - { - Operations.Enqueue(new RemoveOperation(Context, entity)); - } - - /// - public void RemoveRange(IEnumerable entities) - { - Operations.Enqueue(new RemoveRangeOperation(Context, entities)); - } - - /// - /// Finds a of repository by the type of target entity. - /// - /// The type of target entity. - /// DbSet of repository for the entity. - protected DbSet JoinRepository() where TAnotherEntity : class - { - return Context.GetRepository().Table; - } - - /// - /// Finds a on context by the target type of entites. - /// - /// The type of target entity. - /// DbSet of repository for the entity. - protected DbSet JoinDbSet() where TAnotherEntity : class - { - return Context.Set(); - } - } - - /// - /// Generic implementation of Repository controlling the data type of primary key. - /// - /// The type of entity. - /// The type of primary key of entity. - public abstract class Repository : Repository, IRepository where TEntity : class - { - /// - /// - /// - /// - protected Repository(RepositoryContext dbContext) : base(dbContext) - { - } - - /// - public async Task Get(TKey id) - { - return await Table.FindAsync(id).AsTask(); - } - } - - /// - /// Repository supporting identification via composite PK. - /// - /// Data type of used entity - /// Data type of first part of PK - /// Data type of second part of PK - public abstract class Repository : Repository, IRepository where TEntity : class - { - /// - /// - /// - /// - protected Repository(RepositoryContext dbContext) : base(dbContext) - { - } - - /// - public async Task Get(TFirstKey firstKey, TSecondKey secondKey) - { - return await Table.FindAsync(firstKey, secondKey).AsTask(); - } - } \ No newline at end of file diff --git a/Satistools.Model.Repository/RepositoryContext.cs b/Satistools.Model.Repository/RepositoryContext.cs deleted file mode 100644 index f1867d6..0000000 --- a/Satistools.Model.Repository/RepositoryContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.ObjectModel; -using Microsoft.EntityFrameworkCore; - -namespace Satistools.Model.Repository; - -/// -/// Database Context expanding its workflow by registered repositories. -/// -public abstract class RepositoryContext : DbContext -{ - /// - /// Dictionary of registered repositories assigned to its types. - /// - private readonly IDictionary _repositories; - - protected RepositoryContext() - { - _repositories = new ReadOnlyDictionary(new Dictionary()); - } - - /// - /// - /// - /// - /// - protected RepositoryContext(DbContextOptions options, IEnumerable repositories) : base(options) - { - _repositories = new ReadOnlyDictionary(repositories.ToDictionary(repo => repo.EntityType, repo => repo)); - } - - /// - /// Gets target repository by the type of entity. - /// - /// The type of used entity. - /// Found instance of entity. - public IRepository GetRepository() where TEntity : class - { - return (IRepository) _repositories[typeof(TEntity)]; - } -} \ No newline at end of file diff --git a/Satistools.Model.Repository/RepositoryPatternBuilderOptions.cs b/Satistools.Model.Repository/RepositoryPatternBuilderOptions.cs deleted file mode 100644 index 6254351..0000000 --- a/Satistools.Model.Repository/RepositoryPatternBuilderOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Satistools.Model.Repository -{ - /// - /// Options for registering a repository pattern. - /// - public class RepositoryPatternBuilderOptions - { - internal RepositoryPatternBuilderOptions(IServiceCollection services) - { - Services = services; - } - - /// - /// Collection of registered services. - /// - private IServiceCollection Services { get; } - - /// - /// Register a new repository to services as direct type. - /// - /// Implementation type of repository. - public void AddRepository() where TImplementation : class, IRepository - { - AddRepository(); - } - - /// - /// Registers a new repository to services. - /// - /// Interface of repository - /// The implementation type of repository. - public void AddRepository() - where TInterface : class, IRepository - where TImplementation : class, TInterface - { - Services.AddScoped(); - } - } -} \ No newline at end of file diff --git a/Satistools.ModelTest/Satistools.ModelTest.csproj b/Satistools.ModelTest/Satistools.ModelTest.csproj deleted file mode 100644 index be96d4b..0000000 --- a/Satistools.ModelTest/Satistools.ModelTest.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - diff --git a/Satistools.ModelTest/TestFactory.cs b/Satistools.ModelTest/TestFactory.cs deleted file mode 100644 index 666ccad..0000000 --- a/Satistools.ModelTest/TestFactory.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Satistools.ModelTest; - -/// -/// Factory class preparing the environment for unit tests. -/// -public abstract class TestFactory -{ - /// - /// Collection for building - /// - private readonly ServiceCollection _serviceCollection = new(); - - /// - /// Backing field for - /// - private IServiceProvider? _serviceProvider; - - /// - /// Initializes a new instance of with configured services. - /// - protected TestFactory() - { - } - - /// - /// Access to Dependency Container. - /// - /// - /// Provider is created on demand. If the provider has no instance yet, configuration and building process - /// will be called. - /// - public IServiceProvider ServiceProvider - { - get - { - if (_serviceProvider is null) - { - OnConfiguring(_serviceCollection); - _serviceProvider = _serviceCollection.BuildServiceProvider(); - } - - return _serviceProvider; - } - } - - /// - /// Method providing additional way to register custom services. - /// - /// Services collection - protected virtual void OnConfiguring(ServiceCollection services) - { - } - - /// - /// Loads up a default connection string from and adds - /// randomly generated database name to ensure each test will be running in separate database - /// context. - /// - /// Default connection string. - /// Unable to determine current location. - protected static string GetDatabaseName() - { - return $"xunit_{Guid.NewGuid():N}"; - } -} \ No newline at end of file diff --git a/Satistools.ModelTest/TestFixture.cs b/Satistools.ModelTest/TestFixture.cs deleted file mode 100644 index 48558c8..0000000 --- a/Satistools.ModelTest/TestFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Xunit.Abstractions; - -namespace Satistools.ModelTest; - -/// -/// Base class for all unit tests -/// -/// The type of used test factory -/// The type of used database context class -public abstract class TestFixture : IClassFixture, IAsyncLifetime - where TFactory : TestFactory - where TDbContext : DbContext -{ - protected TestFixture(TFactory factory, ITestOutputHelper testOutputHelper) - { - Factory = factory; - TestOutputHelper = testOutputHelper; - ServiceProvider = factory.ServiceProvider.CreateScope().ServiceProvider; - Context = ServiceProvider.GetRequiredService(); - } - - /// - /// Instance of Factory creating Test Context. - /// - protected TFactory Factory { get; } - - /// - /// Instance of for output debugging purposes. - /// - protected ITestOutputHelper TestOutputHelper { get; } - - /// - /// Instance of new Scope of in current context. - /// - protected IServiceProvider ServiceProvider { get; } - - /// - /// Instance of actual used - /// - protected TDbContext Context { get; } - - /// - public virtual async Task InitializeAsync() - { - await Context.Database.EnsureCreatedAsync(); - } - - /// - public virtual async Task DisposeAsync() - { - await Context.Database.EnsureDeletedAsync(); - } -} \ No newline at end of file diff --git a/Satistools.Web/Satistools.Web.csproj b/Satistools.Web/Satistools.Web.csproj index d26060e..8c6531a 100644 --- a/Satistools.Web/Satistools.Web.csproj +++ b/Satistools.Web/Satistools.Web.csproj @@ -30,6 +30,7 @@ + diff --git a/Satistools.Web/Startup.cs b/Satistools.Web/Startup.cs index 3f81dca..3e52241 100644 --- a/Satistools.Web/Startup.cs +++ b/Satistools.Web/Startup.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; +using Satistools.Calculator.Extensions; using Satistools.GameData; using Satistools.GameData.Extensions; @@ -19,6 +20,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddGameDataModel(_configuration); + services.AddCalculator(); } public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/Satistools.sln b/Satistools.sln index 3e2ec49..446b1ad 100644 --- a/Satistools.sln +++ b/Satistools.sln @@ -17,17 +17,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.DataReader.Test" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.GameData.Test", "Satistools.GameData.Test\Satistools.GameData.Test.csproj", "{1A5B0491-7105-40A0-BAFA-2EE067D8DF4A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{E74876A2-78FA-4AC0-845F-4364E12ACBF7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.ModelTest", "Satistools.ModelTest\Satistools.ModelTest.csproj", "{7680AEEC-B421-445B-A144-ED226614382B}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.Web", "Satistools.Web\Satistools.Web.csproj", "{D9906108-E5E6-4485-AD7C-AA24689910DD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{B7495254-A6B0-4A1A-87BD-40C8F34BAE95}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.ImageMove", "Satistools.ImageMove\Satistools.ImageMove.csproj", "{8E0C4DF3-36FC-4CF5-BA1A-3047CDAB13B3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.Model.Repository", "Satistools.Model.Repository\Satistools.Model.Repository.csproj", "{FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.Calculator", "Satistools.Calculator\Satistools.Calculator.csproj", "{6CC7143E-7EC0-48C4-9AA5-A8F5BC5ED088}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Satistools.Calculator.Test", "Satistools.Calculator.Test\Satistools.Calculator.Test.csproj", "{42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -51,10 +49,6 @@ Global {1A5B0491-7105-40A0-BAFA-2EE067D8DF4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A5B0491-7105-40A0-BAFA-2EE067D8DF4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A5B0491-7105-40A0-BAFA-2EE067D8DF4A}.Release|Any CPU.Build.0 = Release|Any CPU - {7680AEEC-B421-445B-A144-ED226614382B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7680AEEC-B421-445B-A144-ED226614382B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7680AEEC-B421-445B-A144-ED226614382B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7680AEEC-B421-445B-A144-ED226614382B}.Release|Any CPU.Build.0 = Release|Any CPU {D9906108-E5E6-4485-AD7C-AA24689910DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9906108-E5E6-4485-AD7C-AA24689910DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9906108-E5E6-4485-AD7C-AA24689910DD}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -63,16 +57,19 @@ Global {8E0C4DF3-36FC-4CF5-BA1A-3047CDAB13B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E0C4DF3-36FC-4CF5-BA1A-3047CDAB13B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E0C4DF3-36FC-4CF5-BA1A-3047CDAB13B3}.Release|Any CPU.Build.0 = Release|Any CPU - {FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8}.Release|Any CPU.Build.0 = Release|Any CPU + {6CC7143E-7EC0-48C4-9AA5-A8F5BC5ED088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CC7143E-7EC0-48C4-9AA5-A8F5BC5ED088}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CC7143E-7EC0-48C4-9AA5-A8F5BC5ED088}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CC7143E-7EC0-48C4-9AA5-A8F5BC5ED088}.Release|Any CPU.Build.0 = Release|Any CPU + {42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {852B5F63-A368-42FB-A2AF-38C8F599D436} = {1B36C950-076F-4D86-9875-87248184E888} {1A5B0491-7105-40A0-BAFA-2EE067D8DF4A} = {1B36C950-076F-4D86-9875-87248184E888} - {7680AEEC-B421-445B-A144-ED226614382B} = {E74876A2-78FA-4AC0-845F-4364E12ACBF7} {8E0C4DF3-36FC-4CF5-BA1A-3047CDAB13B3} = {B7495254-A6B0-4A1A-87BD-40C8F34BAE95} - {FA5ED6EC-F9E2-4D76-8D36-42EB0BE79FF8} = {E74876A2-78FA-4AC0-845F-4364E12ACBF7} + {42B8821B-B2C4-4C4E-9D65-C0B22AE8E8B4} = {1B36C950-076F-4D86-9875-87248184E888} EndGlobalSection EndGlobal diff --git a/Satistools.sln.DotSettings.user b/Satistools.sln.DotSettings.user index d9f5df6..3022b46 100644 --- a/Satistools.sln.DotSettings.user +++ b/Satistools.sln.DotSettings.user @@ -1,15 +1,7 @@  - C:\Users\Zechy\AppData\Local\JetBrains\Rider2022.1\resharper-host\temp\Rider\vAny\CoverageData\_Satistools.652722255\Snapshot\snapshot.utdcvr - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test_ReadJson" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>NUnit3x::852B5F63-A368-42FB-A2AF-38C8F599D436::net6.0::Satistools.DataReader.Test.DataParserTest</TestId> - <TestId>NUnit3x::852B5F63-A368-42FB-A2AF-38C8F599D436::net6.0::Satistools.DataReader.Test.Others.RegexTest.Test_ColorRegexGroups</TestId> - <TestId>NUnit3x::852B5F63-A368-42FB-A2AF-38C8F599D436::net6.0::Satistools.DataReader.Test.RegexTest.Test_Regex</TestId> - <TestId>NUnit3x::852B5F63-A368-42FB-A2AF-38C8F599D436::net6.0::Satistools.DataReader.Test.EntityResolverTest.Test_ResolveEntities</TestId> - <TestId>xUnit::1A5B0491-7105-40A0-BAFA-2EE067D8DF4A::net6.0::Satistools.GameData.Test.ItemTest.Test_CRUD</TestId> - <TestId>xUnit::1A5B0491-7105-40A0-BAFA-2EE067D8DF4A::net6.0::Satistools.GameData.Test.RecipeTest.Test_CRUD</TestId> - <TestId>xUnit::1A5B0491-7105-40A0-BAFA-2EE067D8DF4A::net6.0::Satistools.GameData.Test.BuildableManufacturerTest.Test_CRUD</TestId> - <TestId>xUnit::1A5B0491-7105-40A0-BAFA-2EE067D8DF4A::net6.0::Satistools.GameData.Test.RecipeMapperTest.Test_RecipeMapper</TestId> - <TestId>xUnit::1A5B0491-7105-40A0-BAFA-2EE067D8DF4A::net6.0::Satistools.GameData.Test.Helpers.ColorHelperTest.Test_FromHexaString</TestId> - </TestAncestor> -</SessionState> \ No newline at end of file + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;Tests&gt;\&lt;Satistools.Calculator.Test&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Project Location="E:\WorkingDir\RiderProjects\Satistools\Satistools.Calculator.Test" Presentation="&lt;Tests&gt;\&lt;Satistools.Calculator.Test&gt;" /> +</SessionState> + + \ No newline at end of file