294 lines
11 KiB
C#
294 lines
11 KiB
C#
using System.Text.Json;
|
|
using Microsoft.Extensions.Options;
|
|
using SpaceGame.Api.Shared.Runtime;
|
|
|
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
|
|
|
public sealed class StaticDataProvider : IStaticDataProvider
|
|
{
|
|
private readonly string _dataRoot;
|
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
public StaticDataProvider(IOptions<StaticDataOptions> staticDataOptions)
|
|
{
|
|
_dataRoot = staticDataOptions.Value.DataRoot;
|
|
|
|
var knownSystems = Read<List<SolarSystemDefinition>>("systems.json");
|
|
var races = Read<List<RaceDefinition>>("races.json");
|
|
var factions = Read<List<FactionDefinition>>("factions.json");
|
|
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
|
var ships = Read<List<ShipDefinition>>("ships.json");
|
|
var items = Read<List<ItemDefinition>>("items.json");
|
|
var recipes = BuildRecipes(items, ships, modules);
|
|
var moduleRecipes = BuildModuleRecipes(modules);
|
|
|
|
KnownSystems = knownSystems;
|
|
RaceDefinitions = races.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
FactionDefinitions = factions.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
ModuleDefinitions = modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
ShipDefinitions = ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
ItemDefinitions = items.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
Recipes = recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
|
ModuleRecipes = moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal);
|
|
ProductionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
|
|
}
|
|
|
|
public IReadOnlyList<SolarSystemDefinition> KnownSystems { get; }
|
|
|
|
public IReadOnlyDictionary<string, RaceDefinition> RaceDefinitions { get; }
|
|
|
|
public IReadOnlyDictionary<string, FactionDefinition> FactionDefinitions { get; }
|
|
|
|
public IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions { get; }
|
|
|
|
public IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions { get; }
|
|
|
|
public IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions { get; }
|
|
|
|
public IReadOnlyDictionary<string, RecipeDefinition> Recipes { get; }
|
|
|
|
public IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; }
|
|
|
|
public ProductionGraph ProductionGraph { get; }
|
|
|
|
private T Read<T>(string fileName)
|
|
{
|
|
var path = Path.Combine(_dataRoot, fileName);
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
|
|
?? throw new InvalidOperationException($"Unable to read {fileName}.");
|
|
}
|
|
|
|
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
|
|
modules
|
|
.Where(module => module.BuildRecipes.Count > 0)
|
|
.Select(module => new ModuleRecipeDefinition
|
|
{
|
|
ModuleId = module.Id,
|
|
Duration = module.BuildRecipes[0].Time,
|
|
Inputs = module.BuildRecipes[0].Wares
|
|
.Select(input => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
})
|
|
.ToList();
|
|
|
|
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
|
|
{
|
|
var recipes = new List<RecipeDefinition>();
|
|
var preferredProducerByItemId = modules
|
|
.Where(module => module.ProductItemIds.Count > 0)
|
|
.GroupBy(module => module.ProductItemIds[0], StringComparer.Ordinal)
|
|
.ToDictionary(
|
|
group => group.Key,
|
|
group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id,
|
|
StringComparer.Ordinal);
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (item.Production.Count > 0)
|
|
{
|
|
foreach (var production in item.Production)
|
|
{
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = $"{item.Id}-{production.Method}-production",
|
|
Label = production.Name == "Universal" ? item.Name : $"{item.Name} ({production.Name})",
|
|
FacilityCategory = InferFacilityCategory(item),
|
|
Duration = production.Time,
|
|
Priority = InferRecipePriority(item),
|
|
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
|
|
Inputs = production.Wares
|
|
.Select(input => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
Outputs =
|
|
[
|
|
new RecipeOutputDefinition
|
|
{
|
|
ItemId = item.Id,
|
|
Amount = production.Amount,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (item.Construction is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = item.Construction.RecipeId ?? $"{item.Id}-production",
|
|
Label = item.Name,
|
|
FacilityCategory = item.Construction.FacilityCategory,
|
|
Duration = item.Construction.CycleTime,
|
|
Priority = item.Construction.Priority,
|
|
RequiredModules = item.Construction.RequiredModules.ToList(),
|
|
Inputs = item.Construction.Requirements
|
|
.Select(input => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
Outputs =
|
|
[
|
|
new RecipeOutputDefinition
|
|
{
|
|
ItemId = item.Id,
|
|
Amount = item.Construction.BatchSize,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
foreach (var ship in ships)
|
|
{
|
|
foreach (var production in ship.Production)
|
|
{
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = $"{ship.Id}-{production.Method}-construction",
|
|
Label = $"{ship.Label} Construction",
|
|
FacilityCategory = "shipyard",
|
|
Duration = production.Time,
|
|
Priority = InferShipRecipePriority(ship),
|
|
RequiredModules = InferShipBuildModules(ship),
|
|
Inputs = production.Wares
|
|
.Select(input => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
ShipOutputId = ship.Id,
|
|
});
|
|
}
|
|
}
|
|
|
|
return recipes;
|
|
}
|
|
|
|
private static string InferFacilityCategory(ItemDefinition item) =>
|
|
item.Group switch
|
|
{
|
|
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
|
|
_ => "station",
|
|
};
|
|
|
|
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
|
|
{
|
|
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
|
|
{
|
|
return [moduleId];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static int InferRecipePriority(ItemDefinition item) =>
|
|
item.Group switch
|
|
{
|
|
"energy" => 140,
|
|
"water" => 130,
|
|
"food" => 120,
|
|
"agricultural" => 110,
|
|
"refined" => 100,
|
|
"hightech" => 90,
|
|
"shiptech" => 80,
|
|
"pharmaceutical" => 70,
|
|
_ => 60,
|
|
};
|
|
|
|
private static List<string> InferShipBuildModules(ShipDefinition ship) =>
|
|
ship.Size switch
|
|
{
|
|
"extrasmall" or "small" or "medium" => ["module_gen_build_dockarea_m_01"],
|
|
"large" => ["module_gen_build_l_01"],
|
|
"extralarge" => ["module_gen_build_xl_01"],
|
|
_ => ["module_gen_build_dockarea_m_01"],
|
|
};
|
|
|
|
private static int InferShipRecipePriority(ShipDefinition ship) =>
|
|
ship.Kind switch
|
|
{
|
|
"military" => 170,
|
|
"construction" => 140,
|
|
"transport" => 120,
|
|
"mining" => 110,
|
|
_ => 100,
|
|
};
|
|
|
|
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
|
|
{
|
|
for (var index = 0; index < modules.Count; index += 1)
|
|
{
|
|
var module = modules[index];
|
|
try
|
|
{
|
|
module.ModuleType = module.Type.ToModuleType();
|
|
}
|
|
catch (ArgumentOutOfRangeException exception)
|
|
{
|
|
throw new InvalidOperationException($"Module '{module.Id}' has unsupported type '{module.Type}'.", exception);
|
|
}
|
|
|
|
module.Type = module.ModuleType.ToDataValue();
|
|
modules[index] = CreateSpecializedModuleDefinition(module);
|
|
}
|
|
|
|
return modules;
|
|
}
|
|
|
|
private static ModuleDefinition CreateSpecializedModuleDefinition(ModuleDefinition module)
|
|
{
|
|
if (module.ModuleType == ModuleType.Storage)
|
|
{
|
|
if (module.Cargo is null)
|
|
{
|
|
throw new InvalidOperationException($"Storage module '{module.Id}' is missing cargo metadata.");
|
|
}
|
|
|
|
try
|
|
{
|
|
return new StorageModuleDefinition(module, module.Cargo.Type.ToStorageKind(), module.Cargo.Max);
|
|
}
|
|
catch (ArgumentOutOfRangeException exception)
|
|
{
|
|
throw new InvalidOperationException($"Storage module '{module.Id}' has unsupported cargo type '{module.Cargo.Type}'.", exception);
|
|
}
|
|
}
|
|
|
|
if (module.ModuleType == ModuleType.Habitation)
|
|
{
|
|
return new HabitationModuleDefinition(module, module.SerializedWorkforce?.SupportedPopulation ?? 0f);
|
|
}
|
|
|
|
if (module.ModuleType == ModuleType.BuildModule)
|
|
{
|
|
return new BuildModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
|
|
}
|
|
|
|
if (module.ModuleType == ModuleType.Production)
|
|
{
|
|
return new ProductionModuleDefinition(module, module.SerializedWorkforce?.RequiredWorkforce ?? 0f);
|
|
}
|
|
|
|
return module;
|
|
}
|
|
}
|