chore: add .editorconfig and consistent formatting for backend projects
Adds an `.editorconfig` file with C# and project-specific conventions. Applies consistent indentation and formatting across backend handlers, runtime models, and AI services.
This commit is contained in:
@@ -2,53 +2,53 @@ namespace SpaceGame.Api.Industry.Planning;
|
||||
|
||||
internal static class CommodityOperationalSignal
|
||||
{
|
||||
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
|
||||
var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||
var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
|
||||
|
||||
var levelWeight = commodity.Level switch
|
||||
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
CommodityLevelKind.Critical => 140f,
|
||||
CommodityLevelKind.Low => 80f,
|
||||
CommodityLevelKind.Stable => 20f,
|
||||
_ => 0f,
|
||||
};
|
||||
var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
|
||||
var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||
var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
|
||||
|
||||
return levelWeight
|
||||
+ (productionDeficit * 140f)
|
||||
+ (levelDeficit * 120f)
|
||||
+ backlogPressure;
|
||||
}
|
||||
var levelWeight = commodity.Level switch
|
||||
{
|
||||
CommodityLevelKind.Critical => 140f,
|
||||
CommodityLevelKind.Low => 80f,
|
||||
CommodityLevelKind.Stable => 20f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
commodity.ProjectedProductionRatePerSecond > 0.01f
|
||||
&& commodity.ProjectedNetRatePerSecond >= -0.01f
|
||||
&& commodity.LevelSeconds >= targetLevelSeconds
|
||||
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
|
||||
|
||||
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
!IsOperational(commodity, targetLevelSeconds)
|
||||
|| commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
|
||||
|
||||
internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
|
||||
{
|
||||
return 0.65f;
|
||||
return levelWeight
|
||||
+ (productionDeficit * 140f)
|
||||
+ (levelDeficit * 120f)
|
||||
+ backlogPressure;
|
||||
}
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Critical)
|
||||
{
|
||||
return 0.72f;
|
||||
}
|
||||
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
commodity.ProjectedProductionRatePerSecond > 0.01f
|
||||
&& commodity.ProjectedNetRatePerSecond >= -0.01f
|
||||
&& commodity.LevelSeconds >= targetLevelSeconds
|
||||
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
|
||||
{
|
||||
return 0.84f;
|
||||
}
|
||||
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||
!IsOperational(commodity, targetLevelSeconds)
|
||||
|| commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
|
||||
|
||||
return 1f;
|
||||
}
|
||||
internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||
{
|
||||
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
|
||||
{
|
||||
return 0.65f;
|
||||
}
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Critical)
|
||||
{
|
||||
return 0.72f;
|
||||
}
|
||||
|
||||
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
|
||||
{
|
||||
return 0.84f;
|
||||
}
|
||||
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,202 +4,202 @@ using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
internal sealed class FactionEconomySnapshot
|
||||
{
|
||||
private readonly Dictionary<string, FactionCommoditySnapshot> commodities = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, FactionCommoditySnapshot> commodities = new(StringComparer.Ordinal);
|
||||
|
||||
internal IReadOnlyDictionary<string, FactionCommoditySnapshot> Commodities => commodities;
|
||||
internal IReadOnlyDictionary<string, FactionCommoditySnapshot> Commodities => commodities;
|
||||
|
||||
internal FactionCommoditySnapshot GetCommodity(string itemId)
|
||||
{
|
||||
if (!commodities.TryGetValue(itemId, out var commodity))
|
||||
internal FactionCommoditySnapshot GetCommodity(string itemId)
|
||||
{
|
||||
commodity = new FactionCommoditySnapshot(itemId);
|
||||
commodities[itemId] = commodity;
|
||||
}
|
||||
if (!commodities.TryGetValue(itemId, out var commodity))
|
||||
{
|
||||
commodity = new FactionCommoditySnapshot(itemId);
|
||||
commodities[itemId] = commodity;
|
||||
}
|
||||
|
||||
return commodity;
|
||||
}
|
||||
return commodity;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FactionCommoditySnapshot
|
||||
{
|
||||
internal FactionCommoditySnapshot(string itemId)
|
||||
{
|
||||
ItemId = itemId;
|
||||
}
|
||||
internal FactionCommoditySnapshot(string itemId)
|
||||
{
|
||||
ItemId = itemId;
|
||||
}
|
||||
|
||||
internal string ItemId { get; }
|
||||
internal float OnHand { get; set; }
|
||||
internal float ReservedForConstruction { get; set; }
|
||||
internal float BuyBacklog { get; set; }
|
||||
internal float SellBacklog { get; set; }
|
||||
internal float Inbound { get; set; }
|
||||
internal float ProductionRatePerSecond { get; set; }
|
||||
internal float CommittedProductionRatePerSecond { get; set; }
|
||||
internal float ConsumptionRatePerSecond { get; set; }
|
||||
internal string ItemId { get; }
|
||||
internal float OnHand { get; set; }
|
||||
internal float ReservedForConstruction { get; set; }
|
||||
internal float BuyBacklog { get; set; }
|
||||
internal float SellBacklog { get; set; }
|
||||
internal float Inbound { get; set; }
|
||||
internal float ProductionRatePerSecond { get; set; }
|
||||
internal float CommittedProductionRatePerSecond { get; set; }
|
||||
internal float ConsumptionRatePerSecond { get; set; }
|
||||
|
||||
internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction);
|
||||
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
|
||||
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
|
||||
internal float LevelSeconds => AvailableStock <= 0.01f
|
||||
? 0f
|
||||
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
|
||||
internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction);
|
||||
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
|
||||
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
|
||||
internal float LevelSeconds => AvailableStock <= 0.01f
|
||||
? 0f
|
||||
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
|
||||
|
||||
internal CommodityLevelKind Level =>
|
||||
LevelSeconds switch
|
||||
{
|
||||
<= 60f => CommodityLevelKind.Critical,
|
||||
<= 180f => CommodityLevelKind.Low,
|
||||
<= 480f => CommodityLevelKind.Stable,
|
||||
_ => CommodityLevelKind.Surplus,
|
||||
};
|
||||
internal CommodityLevelKind Level =>
|
||||
LevelSeconds switch
|
||||
{
|
||||
<= 60f => CommodityLevelKind.Critical,
|
||||
<= 180f => CommodityLevelKind.Low,
|
||||
<= 480f => CommodityLevelKind.Stable,
|
||||
_ => CommodityLevelKind.Surplus,
|
||||
};
|
||||
}
|
||||
|
||||
internal enum CommodityLevelKind
|
||||
{
|
||||
Critical,
|
||||
Low,
|
||||
Stable,
|
||||
Surplus,
|
||||
Critical,
|
||||
Low,
|
||||
Stable,
|
||||
Surplus,
|
||||
}
|
||||
|
||||
internal static class FactionEconomyAnalyzer
|
||||
{
|
||||
internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId)
|
||||
{
|
||||
var snapshot = new FactionEconomySnapshot();
|
||||
|
||||
foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)))
|
||||
internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId)
|
||||
{
|
||||
foreach (var (itemId, amount) in station.Inventory)
|
||||
{
|
||||
snapshot.GetCommodity(itemId).OnHand += amount;
|
||||
}
|
||||
var snapshot = new FactionEconomySnapshot();
|
||||
|
||||
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
|
||||
{
|
||||
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
|
||||
if (recipe is null)
|
||||
foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
foreach (var (itemId, amount) in station.Inventory)
|
||||
{
|
||||
snapshot.GetCommodity(itemId).OnHand += amount;
|
||||
}
|
||||
|
||||
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
|
||||
{
|
||||
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
|
||||
if (recipe is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
|
||||
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
|
||||
if (cyclesPerSecond <= 0.0001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
|
||||
}
|
||||
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
|
||||
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
|
||||
if (cyclesPerSecond <= 0.0001f)
|
||||
foreach (var order in world.MarketOrders.Where(order =>
|
||||
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& order.State != MarketOrderStateKinds.Cancelled
|
||||
&& order.RemainingAmount > 0.01f))
|
||||
{
|
||||
continue;
|
||||
var commodity = snapshot.GetCommodity(order.ItemId);
|
||||
if (string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal))
|
||||
{
|
||||
commodity.BuyBacklog += order.RemainingAmount;
|
||||
}
|
||||
else if (string.Equals(order.Kind, MarketOrderKinds.Sell, StringComparison.Ordinal))
|
||||
{
|
||||
commodity.SellBacklog += order.RemainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
foreach (var site in world.ConstructionSites.Where(site =>
|
||||
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed))
|
||||
{
|
||||
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
|
||||
ApplyCommittedProduction(world, snapshot, site);
|
||||
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
|
||||
if (remaining > 0.01f)
|
||||
{
|
||||
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var output in recipe.Outputs)
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void ApplyCommittedProduction(
|
||||
SimulationWorld world,
|
||||
FactionEconomySnapshot snapshot,
|
||||
ConstructionSiteRuntime site)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.BlueprintId)
|
||||
|| !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
|
||||
{
|
||||
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var order in world.MarketOrders.Where(order =>
|
||||
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& order.State != MarketOrderStateKinds.Cancelled
|
||||
&& order.RemainingAmount > 0.01f))
|
||||
{
|
||||
var commodity = snapshot.GetCommodity(order.ItemId);
|
||||
if (string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal))
|
||||
{
|
||||
commodity.BuyBacklog += order.RemainingAmount;
|
||||
}
|
||||
else if (string.Equals(order.Kind, MarketOrderKinds.Sell, StringComparison.Ordinal))
|
||||
{
|
||||
commodity.SellBacklog += order.RemainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var site in world.ConstructionSites.Where(site =>
|
||||
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed))
|
||||
{
|
||||
ApplyCommittedProduction(world, snapshot, site);
|
||||
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
|
||||
if (remaining > 0.01f)
|
||||
var recipeOutputs = world.Recipes.Values
|
||||
.Where(candidate => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, candidate), site.BlueprintId, StringComparison.Ordinal))
|
||||
.SelectMany(candidate => candidate.Outputs)
|
||||
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
|
||||
if (recipeOutputs.Count == 0)
|
||||
{
|
||||
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
|
||||
return;
|
||||
}
|
||||
|
||||
var materialFraction = 0f;
|
||||
var materialTerms = 0;
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
materialTerms += 1;
|
||||
materialFraction += required.Value <= 0.01f
|
||||
? 1f
|
||||
: Math.Clamp(GetConstructionDeliveredAmount(world, site, required.Key) / required.Value, 0f, 1f);
|
||||
}
|
||||
|
||||
materialFraction = materialTerms == 0 ? 1f : materialFraction / materialTerms;
|
||||
|
||||
var buildFraction = recipe.Duration <= 0.01f
|
||||
? 0f
|
||||
: Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
|
||||
var readiness = site.State switch
|
||||
{
|
||||
ConstructionSiteStateKinds.Active => 0.3f,
|
||||
ConstructionSiteStateKinds.Planned => 0.15f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
readiness += materialFraction * 0.45f;
|
||||
readiness += buildFraction * 0.25f;
|
||||
|
||||
if (site.AssignedConstructorShipIds.Count > 0)
|
||||
{
|
||||
readiness += 0.1f;
|
||||
}
|
||||
|
||||
readiness = Math.Clamp(readiness, 0f, 1f);
|
||||
if (readiness <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cyclesPerSecond = readiness / MathF.Max(recipe.Duration, 0.01f);
|
||||
foreach (var (productItemId, amount) in recipeOutputs)
|
||||
{
|
||||
snapshot.GetCommodity(productItemId).CommittedProductionRatePerSecond += amount * cyclesPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void ApplyCommittedProduction(
|
||||
SimulationWorld world,
|
||||
FactionEconomySnapshot snapshot,
|
||||
ConstructionSiteRuntime site)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.BlueprintId)
|
||||
|| !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var recipeOutputs = world.Recipes.Values
|
||||
.Where(candidate => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, candidate), site.BlueprintId, StringComparison.Ordinal))
|
||||
.SelectMany(candidate => candidate.Outputs)
|
||||
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
|
||||
if (recipeOutputs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var materialFraction = 0f;
|
||||
var materialTerms = 0;
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
materialTerms += 1;
|
||||
materialFraction += required.Value <= 0.01f
|
||||
? 1f
|
||||
: Math.Clamp(GetConstructionDeliveredAmount(world, site, required.Key) / required.Value, 0f, 1f);
|
||||
}
|
||||
|
||||
materialFraction = materialTerms == 0 ? 1f : materialFraction / materialTerms;
|
||||
|
||||
var buildFraction = recipe.Duration <= 0.01f
|
||||
? 0f
|
||||
: Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
|
||||
var readiness = site.State switch
|
||||
{
|
||||
ConstructionSiteStateKinds.Active => 0.3f,
|
||||
ConstructionSiteStateKinds.Planned => 0.15f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
readiness += materialFraction * 0.45f;
|
||||
readiness += buildFraction * 0.25f;
|
||||
|
||||
if (site.AssignedConstructorShipIds.Count > 0)
|
||||
{
|
||||
readiness += 0.1f;
|
||||
}
|
||||
|
||||
readiness = Math.Clamp(readiness, 0f, 1f);
|
||||
if (readiness <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cyclesPerSecond = readiness / MathF.Max(recipe.Duration, 0.01f);
|
||||
foreach (var (productItemId, amount) in recipeOutputs)
|
||||
{
|
||||
snapshot.GetCommodity(productItemId).CommittedProductionRatePerSecond += amount * cyclesPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,52 +2,52 @@ namespace SpaceGame.Api.Industry.Planning;
|
||||
|
||||
public sealed class ProductionGraph
|
||||
{
|
||||
public required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { get; init; }
|
||||
public required IReadOnlyDictionary<string, ProductionProcessNode> Processes { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByOutputId { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByInputId { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; }
|
||||
public required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { get; init; }
|
||||
public required IReadOnlyDictionary<string, ProductionProcessNode> Processes { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByOutputId { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByInputId { get; init; }
|
||||
public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; }
|
||||
|
||||
public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) =>
|
||||
ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : [];
|
||||
public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) =>
|
||||
ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : [];
|
||||
|
||||
public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) =>
|
||||
ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : [];
|
||||
public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) =>
|
||||
ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : [];
|
||||
|
||||
public string? GetPrimaryProducerModule(string itemId) =>
|
||||
GetProcessesForOutput(itemId)
|
||||
.SelectMany(process => process.RequiredModuleIds)
|
||||
.FirstOrDefault();
|
||||
public string? GetPrimaryProducerModule(string itemId) =>
|
||||
GetProcessesForOutput(itemId)
|
||||
.SelectMany(process => process.RequiredModuleIds)
|
||||
.FirstOrDefault();
|
||||
|
||||
public string? GetPrimaryOutputForModule(string moduleId) =>
|
||||
OutputsByModuleId.TryGetValue(moduleId, out var outputs)
|
||||
? outputs.FirstOrDefault()
|
||||
: null;
|
||||
public string? GetPrimaryOutputForModule(string moduleId) =>
|
||||
OutputsByModuleId.TryGetValue(moduleId, out var outputs)
|
||||
? outputs.FirstOrDefault()
|
||||
: null;
|
||||
|
||||
public IReadOnlyList<string> GetImmediateInputs(string itemId) =>
|
||||
GetProcessesForOutput(itemId)
|
||||
.SelectMany(process => process.Inputs.Keys)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
public IReadOnlyList<string> GetImmediateInputs(string itemId) =>
|
||||
GetProcessesForOutput(itemId)
|
||||
.SelectMany(process => process.Inputs.Keys)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public sealed class ProductionCommodityNode
|
||||
{
|
||||
public required string ItemId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Group { get; init; }
|
||||
public required string CargoKind { get; init; }
|
||||
public List<string> ProducerProcessIds { get; } = [];
|
||||
public List<string> ConsumerProcessIds { get; } = [];
|
||||
public required string ItemId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Group { get; init; }
|
||||
public required string CargoKind { get; init; }
|
||||
public List<string> ProducerProcessIds { get; } = [];
|
||||
public List<string> ConsumerProcessIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class ProductionProcessNode
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; init; }
|
||||
public required string FacilityCategory { get; init; }
|
||||
public required IReadOnlyList<string> RequiredModuleIds { get; init; }
|
||||
public required IReadOnlyDictionary<string, float> Inputs { get; init; }
|
||||
public required IReadOnlyDictionary<string, float> Outputs { get; init; }
|
||||
public required bool ProducesShip { get; init; }
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; init; }
|
||||
public required string FacilityCategory { get; init; }
|
||||
public required IReadOnlyList<string> RequiredModuleIds { get; init; }
|
||||
public required IReadOnlyDictionary<string, float> Inputs { get; init; }
|
||||
public required IReadOnlyDictionary<string, float> Outputs { get; init; }
|
||||
public required bool ProducesShip { get; init; }
|
||||
}
|
||||
|
||||
@@ -2,104 +2,104 @@ namespace SpaceGame.Api.Industry.Planning;
|
||||
|
||||
internal static class ProductionGraphBuilder
|
||||
{
|
||||
internal static ProductionGraph Build(
|
||||
IReadOnlyCollection<ItemDefinition> items,
|
||||
IReadOnlyCollection<RecipeDefinition> recipes,
|
||||
IReadOnlyCollection<ModuleDefinition> modules)
|
||||
{
|
||||
var commodities = items.ToDictionary(
|
||||
item => item.Id,
|
||||
item => new ProductionCommodityNode
|
||||
{
|
||||
ItemId = item.Id,
|
||||
Name = item.Name,
|
||||
Group = item.Group,
|
||||
CargoKind = item.CargoKind,
|
||||
},
|
||||
StringComparer.Ordinal);
|
||||
|
||||
var processes = new Dictionary<string, ProductionProcessNode>(StringComparer.Ordinal);
|
||||
var processesByOutputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
|
||||
var processesByInputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
|
||||
var outputsByModuleId = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var recipe in recipes)
|
||||
internal static ProductionGraph Build(
|
||||
IReadOnlyCollection<ItemDefinition> items,
|
||||
IReadOnlyCollection<RecipeDefinition> recipes,
|
||||
IReadOnlyCollection<ModuleDefinition> modules)
|
||||
{
|
||||
var outputs = recipe.Outputs
|
||||
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
|
||||
var inputs = recipe.Inputs
|
||||
.GroupBy(input => input.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(input => input.Amount), StringComparer.Ordinal);
|
||||
var process = new ProductionProcessNode
|
||||
{
|
||||
Id = recipe.Id,
|
||||
Label = recipe.Label,
|
||||
FacilityCategory = recipe.FacilityCategory,
|
||||
RequiredModuleIds = recipe.RequiredModules.ToList(),
|
||||
Inputs = inputs,
|
||||
Outputs = outputs,
|
||||
ProducesShip = recipe.ShipOutputId is not null,
|
||||
};
|
||||
var commodities = items.ToDictionary(
|
||||
item => item.Id,
|
||||
item => new ProductionCommodityNode
|
||||
{
|
||||
ItemId = item.Id,
|
||||
Name = item.Name,
|
||||
Group = item.Group,
|
||||
CargoKind = item.CargoKind,
|
||||
},
|
||||
StringComparer.Ordinal);
|
||||
|
||||
processes[process.Id] = process;
|
||||
var processes = new Dictionary<string, ProductionProcessNode>(StringComparer.Ordinal);
|
||||
var processesByOutputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
|
||||
var processesByInputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
|
||||
var outputsByModuleId = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var output in outputs.Keys)
|
||||
{
|
||||
if (!commodities.ContainsKey(output))
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
continue;
|
||||
var outputs = recipe.Outputs
|
||||
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
|
||||
var inputs = recipe.Inputs
|
||||
.GroupBy(input => input.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(input => input.Amount), StringComparer.Ordinal);
|
||||
var process = new ProductionProcessNode
|
||||
{
|
||||
Id = recipe.Id,
|
||||
Label = recipe.Label,
|
||||
FacilityCategory = recipe.FacilityCategory,
|
||||
RequiredModuleIds = recipe.RequiredModules.ToList(),
|
||||
Inputs = inputs,
|
||||
Outputs = outputs,
|
||||
ProducesShip = recipe.ShipOutputId is not null,
|
||||
};
|
||||
|
||||
processes[process.Id] = process;
|
||||
|
||||
foreach (var output in outputs.Keys)
|
||||
{
|
||||
if (!commodities.ContainsKey(output))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
commodities[output].ProducerProcessIds.Add(process.Id);
|
||||
if (!processesByOutputId.TryGetValue(output, out var outputProcesses))
|
||||
{
|
||||
outputProcesses = [];
|
||||
processesByOutputId[output] = outputProcesses;
|
||||
}
|
||||
|
||||
outputProcesses.Add(process);
|
||||
}
|
||||
|
||||
foreach (var input in inputs.Keys)
|
||||
{
|
||||
if (!commodities.ContainsKey(input))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
commodities[input].ConsumerProcessIds.Add(process.Id);
|
||||
if (!processesByInputId.TryGetValue(input, out var inputProcesses))
|
||||
{
|
||||
inputProcesses = [];
|
||||
processesByInputId[input] = inputProcesses;
|
||||
}
|
||||
|
||||
inputProcesses.Add(process);
|
||||
}
|
||||
}
|
||||
|
||||
commodities[output].ProducerProcessIds.Add(process.Id);
|
||||
if (!processesByOutputId.TryGetValue(output, out var outputProcesses))
|
||||
foreach (var module in modules)
|
||||
{
|
||||
outputProcesses = [];
|
||||
processesByOutputId[output] = outputProcesses;
|
||||
if (!outputsByModuleId.TryGetValue(module.Id, out var outputs))
|
||||
{
|
||||
outputs = new HashSet<string>(StringComparer.Ordinal);
|
||||
outputsByModuleId[module.Id] = outputs;
|
||||
}
|
||||
|
||||
foreach (var product in module.Products)
|
||||
{
|
||||
outputs.Add(product);
|
||||
}
|
||||
}
|
||||
|
||||
outputProcesses.Add(process);
|
||||
}
|
||||
|
||||
foreach (var input in inputs.Keys)
|
||||
{
|
||||
if (!commodities.ContainsKey(input))
|
||||
return new ProductionGraph
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
commodities[input].ConsumerProcessIds.Add(process.Id);
|
||||
if (!processesByInputId.TryGetValue(input, out var inputProcesses))
|
||||
{
|
||||
inputProcesses = [];
|
||||
processesByInputId[input] = inputProcesses;
|
||||
}
|
||||
|
||||
inputProcesses.Add(process);
|
||||
}
|
||||
Commodities = commodities,
|
||||
Processes = processes,
|
||||
ProcessesByOutputId = processesByOutputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
|
||||
ProcessesByInputId = processesByInputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
|
||||
OutputsByModuleId = outputsByModuleId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<string>)entry.Value.OrderBy(value => value, StringComparer.Ordinal).ToList(), StringComparer.Ordinal),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var module in modules)
|
||||
{
|
||||
if (!outputsByModuleId.TryGetValue(module.Id, out var outputs))
|
||||
{
|
||||
outputs = new HashSet<string>(StringComparer.Ordinal);
|
||||
outputsByModuleId[module.Id] = outputs;
|
||||
}
|
||||
|
||||
foreach (var product in module.Products)
|
||||
{
|
||||
outputs.Add(product);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProductionGraph
|
||||
{
|
||||
Commodities = commodities,
|
||||
Processes = processes,
|
||||
ProcessesByOutputId = processesByOutputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
|
||||
ProcessesByInputId = processesByInputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
|
||||
OutputsByModuleId = outputsByModuleId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<string>)entry.Value.OrderBy(value => value, StringComparer.Ordinal).ToList(), StringComparer.Ordinal),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user