to rename
This commit is contained in:
@@ -2,7 +2,6 @@ root = true
|
|||||||
|
|
||||||
[*.{cs,csx}]
|
[*.{cs,csx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = crlf
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
@@ -40,7 +39,6 @@ csharp_new_line_before_open_brace = all
|
|||||||
|
|
||||||
[*.{csproj,props,targets,sln,slnx}]
|
[*.{csproj,props,targets,sln,slnx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = crlf
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
@@ -48,7 +46,6 @@ indent_size = 2
|
|||||||
|
|
||||||
[*.{json,jsonc}]
|
[*.{json,jsonc}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = crlf
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using SpaceGame.Api.Shared.Runtime;
|
using SpaceGame.Api.Shared.Runtime;
|
||||||
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Definitions;
|
namespace SpaceGame.Api.Definitions;
|
||||||
|
|
||||||
@@ -40,19 +41,6 @@ public sealed class ItemProductionDefinition
|
|||||||
public List<ItemEffectDefinition> Effects { get; set; } = [];
|
public List<ItemEffectDefinition> Effects { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BalanceDefinition
|
|
||||||
{
|
|
||||||
public float SimulationSpeedMultiplier { get; set; } = 1f;
|
|
||||||
public float YPlane { get; set; }
|
|
||||||
public float ArrivalThreshold { get; set; }
|
|
||||||
public float MiningRate { get; set; }
|
|
||||||
public float MiningCycleSeconds { get; set; }
|
|
||||||
public float TransferRate { get; set; }
|
|
||||||
public float DockingDuration { get; set; }
|
|
||||||
public float UndockingDuration { get; set; }
|
|
||||||
public float UndockDistance { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StarDefinition
|
public sealed class StarDefinition
|
||||||
{
|
{
|
||||||
public string Kind { get; set; } = "main-sequence";
|
public string Kind { get; set; } = "main-sequence";
|
||||||
@@ -374,8 +362,15 @@ public sealed class ShipDefinition
|
|||||||
public ConstructionDefinition? Construction { get; set; }
|
public ConstructionDefinition? Construction { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class GameStartOptionsDefinition
|
||||||
|
{
|
||||||
|
public int Seed { get; set; } = 1;
|
||||||
|
public WorldGenerationOptions WorldGeneration { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ScenarioDefinition
|
public sealed class ScenarioDefinition
|
||||||
{
|
{
|
||||||
|
public GameStartOptionsDefinition GameStartOptions { get; set; } = new();
|
||||||
public required List<InitialStationDefinition> InitialStations { get; set; }
|
public required List<InitialStationDefinition> InitialStations { get; set; }
|
||||||
public required List<ShipFormationDefinition> ShipFormations { get; set; }
|
public required List<ShipFormationDefinition> ShipFormations { get; set; }
|
||||||
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
|
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using FastEndpoints.Swagger;
|
using FastEndpoints.Swagger;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SpaceGame.Api.Universe.Bootstrap;
|
||||||
using SpaceGame.Api.Universe.Simulation;
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -14,14 +16,29 @@ builder.Services.AddCors((options) =>
|
|||||||
.AllowAnyOrigin();
|
.AllowAnyOrigin();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<StaticDataOptions>()
|
||||||
|
.Bind(builder.Configuration.GetSection("StaticData"))
|
||||||
|
.Validate(options => !string.IsNullOrWhiteSpace(options.DataRoot), "StaticData:DataRoot must be configured.")
|
||||||
|
.ValidateOnStart();
|
||||||
|
builder.Services.Configure<BalanceOptions>(builder.Configuration.GetSection("Balance"));
|
||||||
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
||||||
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
||||||
builder.Services.AddFastEndpoints();
|
|
||||||
builder.Services.SwaggerDocument();
|
builder.Services.AddTransient<SystemGenerationService>();
|
||||||
|
builder.Services.AddTransient<SpatialBuilder>();
|
||||||
|
builder.Services.AddTransient<WorldSeedingService>();
|
||||||
|
builder.Services.AddTransient<ScenarioLoader>();
|
||||||
|
builder.Services.AddTransient<SystemTemplateLoader>();
|
||||||
|
builder.Services.AddTransient<WorldBuilder>();
|
||||||
|
builder.Services.AddSingleton<WorldBootstrapper>();
|
||||||
builder.Services.AddSingleton<WorldService>();
|
builder.Services.AddSingleton<WorldService>();
|
||||||
builder.Services.AddSingleton<TelemetryService>();
|
builder.Services.AddSingleton<TelemetryService>();
|
||||||
builder.Services.AddHostedService<SimulationHostedService>();
|
builder.Services.AddHostedService<SimulationHostedService>();
|
||||||
|
|
||||||
|
builder.Services.AddFastEndpoints();
|
||||||
|
builder.Services.SwaggerDocument();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||||
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Simulation;
|
namespace SpaceGame.Api.Ships.Simulation;
|
||||||
|
|
||||||
internal sealed class ShipAiService
|
public sealed class ShipAiService(
|
||||||
|
IOptions<BalanceOptions> balance)
|
||||||
{
|
{
|
||||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||||
private const float FrigateDps = 7f;
|
private const float FrigateDps = 7f;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Simulation.Core;
|
namespace SpaceGame.Api.Simulation.Core;
|
||||||
|
|
||||||
public sealed class SimulationEngine
|
public sealed class SimulationEngine
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using SpaceGame.Api.Definitions;
|
|
||||||
using SpaceGame.Api.Universe.Simulation;
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Api;
|
namespace SpaceGame.Api.Universe.Api;
|
||||||
|
|
||||||
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition>
|
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceOptions>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
@@ -12,7 +11,7 @@ public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<B
|
|||||||
AllowAnonymous();
|
AllowAnonymous();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
|
public override Task HandleAsync(BalanceOptions req, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var applied = worldService.UpdateBalance(req);
|
var applied = worldService.UpdateBalance(req);
|
||||||
return SendOkAsync(applied, cancellationToken);
|
return SendOkAsync(applied, cancellationToken);
|
||||||
|
|||||||
9
apps/backend/Universe/Bootstrap/StaticDataCatalog.cs
Normal file
9
apps/backend/Universe/Bootstrap/StaticDataCatalog.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
|
public sealed record StaticDataCatalog(
|
||||||
|
IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions,
|
||||||
|
IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions,
|
||||||
|
IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions,
|
||||||
|
IReadOnlyDictionary<string, RecipeDefinition> Recipes,
|
||||||
|
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes,
|
||||||
|
ProductionGraph ProductionGraph);
|
||||||
@@ -1,32 +1,26 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using SpaceGame.Api.Shared.Runtime;
|
using SpaceGame.Api.Shared.Runtime;
|
||||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|
||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
internal sealed class DataCatalogLoader(string dataRoot)
|
internal sealed class StaticDataLoader(IOptions<StaticDataOptions> staticDataOptions)
|
||||||
{
|
{
|
||||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal ScenarioCatalog LoadCatalog()
|
internal StaticDataCatalog Load()
|
||||||
{
|
{
|
||||||
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
|
|
||||||
var scenario = Read<ScenarioDefinition>("scenario.json");
|
|
||||||
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
||||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||||
var items = Read<List<ItemDefinition>>("items.json");
|
var items = Read<List<ItemDefinition>>("items.json");
|
||||||
var balance = Read<BalanceDefinition>("balance.json");
|
|
||||||
var recipes = BuildRecipes(items, ships, modules);
|
var recipes = BuildRecipes(items, ships, modules);
|
||||||
var moduleRecipes = BuildModuleRecipes(modules);
|
var moduleRecipes = BuildModuleRecipes(modules);
|
||||||
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
|
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
|
||||||
|
|
||||||
return new ScenarioCatalog(
|
return new StaticDataCatalog(
|
||||||
authoredSystems,
|
|
||||||
scenario,
|
|
||||||
balance,
|
|
||||||
modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||||
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||||
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||||
@@ -35,67 +29,9 @@ internal sealed class DataCatalogLoader(string dataRoot)
|
|||||||
productionGraph);
|
productionGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ScenarioDefinition NormalizeScenarioToAvailableSystems(
|
|
||||||
ScenarioDefinition scenario,
|
|
||||||
IReadOnlyList<string> availableSystemIds)
|
|
||||||
{
|
|
||||||
if (availableSystemIds.Count == 0)
|
|
||||||
{
|
|
||||||
return scenario;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallbackSystemId = availableSystemIds.Contains("sol", StringComparer.Ordinal)
|
|
||||||
? "sol"
|
|
||||||
: availableSystemIds[0];
|
|
||||||
|
|
||||||
string ResolveSystemId(string systemId) =>
|
|
||||||
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
|
|
||||||
|
|
||||||
return new ScenarioDefinition
|
|
||||||
{
|
|
||||||
InitialStations = scenario.InitialStations
|
|
||||||
.Select(station => new InitialStationDefinition
|
|
||||||
{
|
|
||||||
SystemId = ResolveSystemId(station.SystemId),
|
|
||||||
Label = station.Label,
|
|
||||||
Color = station.Color,
|
|
||||||
Objective = station.Objective,
|
|
||||||
StartingModules = station.StartingModules.ToList(),
|
|
||||||
FactionId = station.FactionId,
|
|
||||||
PlanetIndex = station.PlanetIndex,
|
|
||||||
LagrangeSide = station.LagrangeSide,
|
|
||||||
Position = station.Position?.ToArray(),
|
|
||||||
})
|
|
||||||
.ToList(),
|
|
||||||
ShipFormations = scenario.ShipFormations
|
|
||||||
.Select(formation => new ShipFormationDefinition
|
|
||||||
{
|
|
||||||
ShipId = formation.ShipId,
|
|
||||||
Count = formation.Count,
|
|
||||||
Center = formation.Center.ToArray(),
|
|
||||||
SystemId = ResolveSystemId(formation.SystemId),
|
|
||||||
FactionId = formation.FactionId,
|
|
||||||
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
|
|
||||||
})
|
|
||||||
.ToList(),
|
|
||||||
PatrolRoutes = scenario.PatrolRoutes
|
|
||||||
.Select(route => new PatrolRouteDefinition
|
|
||||||
{
|
|
||||||
SystemId = ResolveSystemId(route.SystemId),
|
|
||||||
Points = route.Points.Select(point => point.ToArray()).ToList(),
|
|
||||||
})
|
|
||||||
.ToList(),
|
|
||||||
MiningDefaults = new MiningDefaultsDefinition
|
|
||||||
{
|
|
||||||
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId),
|
|
||||||
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private T Read<T>(string fileName)
|
private T Read<T>(string fileName)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(dataRoot, fileName);
|
var path = Path.Combine(staticDataOptions.Value.DataRoot, fileName);
|
||||||
var json = File.ReadAllText(path);
|
var json = File.ReadAllText(path);
|
||||||
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
|
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
|
||||||
?? throw new InvalidOperationException($"Unable to read {fileName}.");
|
?? throw new InvalidOperationException($"Unable to read {fileName}.");
|
||||||
@@ -270,7 +206,6 @@ internal sealed class DataCatalogLoader(string dataRoot)
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.Type = module.ModuleType.ToDataValue();
|
module.Type = module.ModuleType.ToDataValue();
|
||||||
|
|
||||||
modules[index] = CreateSpecializedModuleDefinition(module);
|
modules[index] = CreateSpecializedModuleDefinition(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,14 +249,3 @@ internal sealed class DataCatalogLoader(string dataRoot)
|
|||||||
return module;
|
return module;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record ScenarioCatalog(
|
|
||||||
List<SolarSystemDefinition> AuthoredSystems,
|
|
||||||
ScenarioDefinition Scenario,
|
|
||||||
BalanceDefinition Balance,
|
|
||||||
IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions,
|
|
||||||
IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions,
|
|
||||||
IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions,
|
|
||||||
IReadOnlyDictionary<string, RecipeDefinition> Recipes,
|
|
||||||
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes,
|
|
||||||
ProductionGraph ProductionGraph);
|
|
||||||
6
apps/backend/Universe/Bootstrap/StaticDataOptions.cs
Normal file
6
apps/backend/Universe/Bootstrap/StaticDataOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
|
public sealed class StaticDataOptions
|
||||||
|
{
|
||||||
|
public required string DataRoot { get; init; }
|
||||||
|
}
|
||||||
20
apps/backend/Universe/Bootstrap/SystemTemplateLoader.cs
Normal file
20
apps/backend/Universe/Bootstrap/SystemTemplateLoader.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
|
public sealed class SystemTemplateLoader(IOptions<StaticDataOptions> staticDataOptions)
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
internal List<SolarSystemDefinition> Load()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(staticDataOptions.Value.DataRoot, "systems.json");
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<List<SolarSystemDefinition>>(json, _jsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Unable to read systems.json.");
|
||||||
|
}
|
||||||
|
}
|
||||||
48
apps/backend/Universe/Bootstrap/WorldBootstrapper.cs
Normal file
48
apps/backend/Universe/Bootstrap/WorldBootstrapper.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SpaceGame.Api.Universe.Scenario;
|
||||||
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
|
public sealed class WorldBootstrapper
|
||||||
|
{
|
||||||
|
private readonly BalanceOptions _defaultBalance;
|
||||||
|
private readonly WorldGenerationOptions _defaultWorldGeneration;
|
||||||
|
private readonly StaticDataCatalog _staticData;
|
||||||
|
private readonly ScenarioLoader _scenarioLoader;
|
||||||
|
private readonly SystemTemplateLoader _systemTemplateLoader;
|
||||||
|
private readonly WorldBuilder _worldBuilder;
|
||||||
|
|
||||||
|
public WorldBootstrapper(
|
||||||
|
StaticDataCatalog staticData,
|
||||||
|
ScenarioLoader scenarioLoader,
|
||||||
|
SystemTemplateLoader systemTemplateLoader,
|
||||||
|
WorldBuilder worldBuilder,
|
||||||
|
IOptions<BalanceOptions> defaultBalanceOptions,
|
||||||
|
IOptions<WorldGenerationOptions> defaultWorldGenerationOptions)
|
||||||
|
{
|
||||||
|
_defaultBalance = defaultBalanceOptions.Value;
|
||||||
|
_defaultWorldGeneration = defaultWorldGenerationOptions.Value;
|
||||||
|
_staticData = staticData;
|
||||||
|
_scenarioLoader = scenarioLoader;
|
||||||
|
_systemTemplateLoader = systemTemplateLoader;
|
||||||
|
_worldBuilder = worldBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationWorld Bootstrap()
|
||||||
|
{
|
||||||
|
var scenario = _scenarioLoader.Load();
|
||||||
|
var gameStartOptions = scenario?.GameStartOptions ?? new GameStartOptionsDefinition
|
||||||
|
{
|
||||||
|
Seed = 1,
|
||||||
|
WorldGeneration = _defaultWorldGeneration,
|
||||||
|
};
|
||||||
|
|
||||||
|
return _worldBuilder.Build(
|
||||||
|
_staticData,
|
||||||
|
_defaultBalance,
|
||||||
|
_systemTemplateLoader.Load(),
|
||||||
|
gameStartOptions,
|
||||||
|
scenario);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ public sealed class SimulationWorld
|
|||||||
{
|
{
|
||||||
public required string Label { get; init; }
|
public required string Label { get; init; }
|
||||||
public required int Seed { get; init; }
|
public required int Seed { get; init; }
|
||||||
public required BalanceDefinition Balance { get; set; }
|
|
||||||
public required List<SystemRuntime> Systems { get; init; }
|
public required List<SystemRuntime> Systems { get; init; }
|
||||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||||
public required List<CelestialRuntime> Celestials { get; init; }
|
public required List<CelestialRuntime> Celestials { get; init; }
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace SpaceGame.Api.Universe.Scenario;
|
|||||||
internal static class LoaderSupport
|
internal static class LoaderSupport
|
||||||
{
|
{
|
||||||
internal const string DefaultFactionId = "sol-dominion";
|
internal const string DefaultFactionId = "sol-dominion";
|
||||||
internal const int WorldSeed = 1;
|
|
||||||
internal const float MinimumFactionCredits = 0f;
|
internal const float MinimumFactionCredits = 0f;
|
||||||
internal const float MinimumRefineryOre = 0f;
|
internal const float MinimumRefineryOre = 0f;
|
||||||
internal const float MinimumRefineryStock = 0f;
|
internal const float MinimumRefineryStock = 0f;
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
public sealed class ScenarioLoader
|
public sealed class ScenarioLoader(IOptions<StaticDataOptions> staticDataOptions)
|
||||||
{
|
{
|
||||||
private readonly WorldBuilder _worldBuilder;
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
|
||||||
public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null)
|
|
||||||
{
|
{
|
||||||
var generationOptions = worldGeneration ?? new WorldGenerationOptions();
|
PropertyNameCaseInsensitive = true,
|
||||||
var dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
|
};
|
||||||
var dataLoader = new DataCatalogLoader(dataRoot);
|
|
||||||
var generationService = new SystemGenerationService();
|
|
||||||
var spatialBuilder = new SpatialBuilder();
|
|
||||||
var seedingService = new WorldSeedingService();
|
|
||||||
|
|
||||||
_worldBuilder = new WorldBuilder(
|
public ScenarioDefinition? Load()
|
||||||
generationOptions,
|
{
|
||||||
dataLoader,
|
var scenarioPath = Path.Combine(staticDataOptions.Value.DataRoot, "scenario.json");
|
||||||
generationService,
|
if (!File.Exists(scenarioPath))
|
||||||
spatialBuilder,
|
{
|
||||||
seedingService);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SimulationWorld Load() => _worldBuilder.Build();
|
var json = File.ReadAllText(scenarioPath);
|
||||||
|
return JsonSerializer.Deserialize<ScenarioDefinition>(json, _jsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Unable to read scenario.json.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
internal sealed class SpatialBuilder
|
public sealed class SpatialBuilder
|
||||||
{
|
{
|
||||||
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceDefinition balance)
|
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceOptions balance)
|
||||||
{
|
{
|
||||||
var systemGraphs = systems.ToDictionary(
|
var systemGraphs = systems.ToDictionary(
|
||||||
system => system.Definition.Id,
|
system => system.Definition.Id,
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
internal sealed class SystemGenerationService
|
public sealed class SystemGenerationService
|
||||||
{
|
{
|
||||||
private const string SolSystemId = "sol";
|
internal List<SolarSystemDefinition> PrepareAuthoredSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||||
private const string DevelopmentCompanionSystemId = "helios";
|
|
||||||
|
|
||||||
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
|
||||||
authoredSystems
|
authoredSystems
|
||||||
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -71,8 +68,10 @@ internal sealed class SystemGenerationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddById(SolSystemId);
|
foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds)
|
||||||
AddById(DevelopmentCompanionSystemId);
|
{
|
||||||
|
AddById(preferredSystemId);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var system in systems)
|
foreach (var system in systems)
|
||||||
{
|
{
|
||||||
|
|||||||
23
apps/backend/Universe/Scenario/SystemSelectionPolicy.cs
Normal file
23
apps/backend/Universe/Scenario/SystemSelectionPolicy.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
|
internal static class SystemSelectionPolicy
|
||||||
|
{
|
||||||
|
internal static readonly string[] PreferredSystemIds =
|
||||||
|
[
|
||||||
|
"sol",
|
||||||
|
"helios",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static string SelectFallbackSystemId(IReadOnlyList<string> availableSystemIds)
|
||||||
|
{
|
||||||
|
foreach (var preferredSystemId in PreferredSystemIds)
|
||||||
|
{
|
||||||
|
if (availableSystemIds.Contains(preferredSystemId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return preferredSystemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSystemIds.FirstOrDefault() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||||
|
using SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
internal sealed class WorldBuilder(
|
public sealed class WorldBuilder(
|
||||||
WorldGenerationOptions worldGeneration,
|
StaticDataCatalog staticData,
|
||||||
DataCatalogLoader dataLoader,
|
IOptions<BalanceOptions> balance,
|
||||||
SystemGenerationService generationService,
|
SystemGenerationService generationService,
|
||||||
SpatialBuilder spatialBuilder,
|
SpatialBuilder spatialBuilder,
|
||||||
WorldSeedingService seedingService)
|
WorldSeedingService seedingService)
|
||||||
{
|
{
|
||||||
internal SimulationWorld Build()
|
public SimulationWorld Build(
|
||||||
|
GameStartOptionsDefinition gameStartOptions,
|
||||||
|
ScenarioDefinition? scenarioDefinition)
|
||||||
{
|
{
|
||||||
var catalog = dataLoader.LoadCatalog();
|
|
||||||
var systems = generationService.ExpandSystems(
|
var systems = generationService.ExpandSystems(
|
||||||
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
|
generationService.PrepareAuthoredSystems(authoredSystems),
|
||||||
worldGeneration.TargetSystemCount);
|
gameStartOptions.WorldGeneration.TargetSystemCount);
|
||||||
|
|
||||||
var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
|
var scenario = NormalizeScenarioToAvailableSystems(
|
||||||
catalog.Scenario,
|
scenarioDefinition,
|
||||||
systems.Select(system => system.Id).ToList());
|
systems.Select(system => system.Id).ToList());
|
||||||
|
|
||||||
var systemRuntimes = systems
|
var systemRuntimes = systems
|
||||||
@@ -28,22 +31,22 @@ internal sealed class WorldBuilder(
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
|
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
|
||||||
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, catalog.Balance);
|
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, balance.Value);
|
||||||
|
|
||||||
var stations = CreateStations(
|
var stations = CreateStations(
|
||||||
scenario,
|
scenario,
|
||||||
systemsById,
|
systemsById,
|
||||||
spatialLayout.SystemGraphs,
|
spatialLayout.SystemGraphs,
|
||||||
spatialLayout.Celestials,
|
spatialLayout.Celestials,
|
||||||
catalog.ModuleDefinitions,
|
staticData.ModuleDefinitions,
|
||||||
catalog.ItemDefinitions);
|
staticData.ItemDefinitions);
|
||||||
|
|
||||||
seedingService.InitializeStationStockpiles(stations, catalog.ModuleDefinitions);
|
seedingService.InitializeStationStockpiles(stations, staticData.ModuleDefinitions);
|
||||||
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
||||||
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
||||||
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
|
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, staticData.ShipDefinitions, patrolRoutes, stations, refinery);
|
||||||
|
|
||||||
if (worldGeneration.AiControllerFactionCount < int.MaxValue)
|
if (gameStartOptions.WorldGeneration.AiControllerFactionCount < int.MaxValue)
|
||||||
{
|
{
|
||||||
var aiFactionIds = stations
|
var aiFactionIds = stations
|
||||||
.Select(s => s.FactionId)
|
.Select(s => s.FactionId)
|
||||||
@@ -51,7 +54,7 @@ internal sealed class WorldBuilder(
|
|||||||
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.OrderBy(id => id, StringComparer.Ordinal)
|
.OrderBy(id => id, StringComparer.Ordinal)
|
||||||
.Take(worldGeneration.AiControllerFactionCount)
|
.Take(gameStartOptions.WorldGeneration.AiControllerFactionCount)
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
aiFactionIds.Add(DefaultFactionId);
|
aiFactionIds.Add(DefaultFactionId);
|
||||||
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||||
@@ -63,15 +66,14 @@ internal sealed class WorldBuilder(
|
|||||||
var policies = seedingService.CreatePolicies(factions);
|
var policies = seedingService.CreatePolicies(factions);
|
||||||
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
||||||
var nowUtc = DateTimeOffset.UtcNow;
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
var playerFaction = worldGeneration.GeneratePlayerFaction
|
var playerFaction = gameStartOptions.WorldGeneration.GeneratePlayerFaction
|
||||||
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
||||||
: null;
|
: null;
|
||||||
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
||||||
var world = new SimulationWorld
|
var world = new SimulationWorld
|
||||||
{
|
{
|
||||||
Label = "Split Viewer / Simulation World",
|
Label = "Split Viewer / Simulation World",
|
||||||
Seed = WorldSeed,
|
Seed = gameStartOptions.Seed,
|
||||||
Balance = catalog.Balance,
|
|
||||||
Systems = systemRuntimes,
|
Systems = systemRuntimes,
|
||||||
Celestials = spatialLayout.Celestials,
|
Celestials = spatialLayout.Celestials,
|
||||||
Nodes = spatialLayout.Nodes,
|
Nodes = spatialLayout.Nodes,
|
||||||
@@ -86,13 +88,13 @@ internal sealed class WorldBuilder(
|
|||||||
ConstructionSites = [],
|
ConstructionSites = [],
|
||||||
MarketOrders = [],
|
MarketOrders = [],
|
||||||
Policies = policies,
|
Policies = policies,
|
||||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
|
ShipDefinitions = new Dictionary<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
||||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
|
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
||||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
|
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
||||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
|
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
||||||
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
|
Recipes = new Dictionary<string, RecipeDefinition>(staticData.Recipes, StringComparer.Ordinal),
|
||||||
ProductionGraph = catalog.ProductionGraph,
|
ProductionGraph = staticData.ProductionGraph,
|
||||||
OrbitalTimeSeconds = WorldSeed * 97d,
|
OrbitalTimeSeconds = gameStartOptions.Seed * 97d,
|
||||||
GeneratedAtUtc = nowUtc,
|
GeneratedAtUtc = nowUtc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,6 +107,79 @@ internal sealed class WorldBuilder(
|
|||||||
return world;
|
return world;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ScenarioDefinition NormalizeScenarioToAvailableSystems(
|
||||||
|
ScenarioDefinition? scenario,
|
||||||
|
IReadOnlyList<string> availableSystemIds)
|
||||||
|
{
|
||||||
|
var fallbackSystemId = SystemSelectionPolicy.SelectFallbackSystemId(availableSystemIds);
|
||||||
|
|
||||||
|
if (scenario is null)
|
||||||
|
{
|
||||||
|
return new ScenarioDefinition
|
||||||
|
{
|
||||||
|
GameStartOptions = new GameStartOptionsDefinition(),
|
||||||
|
InitialStations = [],
|
||||||
|
ShipFormations = [],
|
||||||
|
PatrolRoutes = [],
|
||||||
|
MiningDefaults = new MiningDefaultsDefinition
|
||||||
|
{
|
||||||
|
NodeSystemId = fallbackSystemId,
|
||||||
|
RefinerySystemId = fallbackSystemId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableSystemIds.Count == 0)
|
||||||
|
{
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ResolveSystemId(string systemId) =>
|
||||||
|
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
|
||||||
|
|
||||||
|
return new ScenarioDefinition
|
||||||
|
{
|
||||||
|
GameStartOptions = scenario.GameStartOptions,
|
||||||
|
InitialStations = scenario.InitialStations
|
||||||
|
.Select(station => new InitialStationDefinition
|
||||||
|
{
|
||||||
|
SystemId = ResolveSystemId(station.SystemId),
|
||||||
|
Label = station.Label,
|
||||||
|
Color = station.Color,
|
||||||
|
Objective = station.Objective,
|
||||||
|
StartingModules = station.StartingModules.ToList(),
|
||||||
|
FactionId = station.FactionId,
|
||||||
|
PlanetIndex = station.PlanetIndex,
|
||||||
|
LagrangeSide = station.LagrangeSide,
|
||||||
|
Position = station.Position?.ToArray(),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
ShipFormations = scenario.ShipFormations
|
||||||
|
.Select(formation => new ShipFormationDefinition
|
||||||
|
{
|
||||||
|
ShipId = formation.ShipId,
|
||||||
|
Count = formation.Count,
|
||||||
|
Center = formation.Center.ToArray(),
|
||||||
|
SystemId = ResolveSystemId(formation.SystemId),
|
||||||
|
FactionId = formation.FactionId,
|
||||||
|
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
PatrolRoutes = scenario.PatrolRoutes
|
||||||
|
.Select(route => new PatrolRouteDefinition
|
||||||
|
{
|
||||||
|
SystemId = ResolveSystemId(route.SystemId),
|
||||||
|
Points = route.Points.Select(point => point.ToArray()).ToList(),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
MiningDefaults = new MiningDefaultsDefinition
|
||||||
|
{
|
||||||
|
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId),
|
||||||
|
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static List<StationRuntime> CreateStations(
|
private static List<StationRuntime> CreateStations(
|
||||||
ScenarioDefinition scenario,
|
ScenarioDefinition scenario,
|
||||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||||
@@ -248,11 +323,10 @@ internal sealed class WorldBuilder(
|
|||||||
StringComparer.Ordinal);
|
StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ShipRuntime> CreateShips(
|
private List<ShipRuntime> CreateShips(
|
||||||
ScenarioDefinition scenario,
|
ScenarioDefinition scenario,
|
||||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||||
BalanceDefinition balance,
|
|
||||||
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
|
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
|
||||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||||
IReadOnlyCollection<StationRuntime> stations,
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
@@ -270,7 +344,7 @@ internal sealed class WorldBuilder(
|
|||||||
|
|
||||||
for (var index = 0; index < formation.Count; index += 1)
|
for (var index = 0; index < formation.Count; index += 1)
|
||||||
{
|
{
|
||||||
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
|
var offset = new Vector3((index % 3) * 18f, balance.Value.YPlane, (index / 3) * 18f);
|
||||||
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
||||||
|
|
||||||
ships.Add(new ShipRuntime
|
ships.Add(new ShipRuntime
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Scenario;
|
namespace SpaceGame.Api.Universe.Scenario;
|
||||||
|
|
||||||
internal sealed class WorldSeedingService
|
public sealed class WorldSeedingService
|
||||||
{
|
{
|
||||||
internal List<FactionRuntime> CreateFactions(
|
internal List<FactionRuntime> CreateFactions(
|
||||||
IReadOnlyCollection<StationRuntime> stations,
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
|
|||||||
14
apps/backend/Universe/Simulation/BalanceOptions.cs
Normal file
14
apps/backend/Universe/Simulation/BalanceOptions.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
public sealed class BalanceOptions
|
||||||
|
{
|
||||||
|
public float SimulationSpeedMultiplier { get; set; } = 1f;
|
||||||
|
public float YPlane { get; set; }
|
||||||
|
public float ArrivalThreshold { get; set; }
|
||||||
|
public float MiningRate { get; set; }
|
||||||
|
public float MiningCycleSeconds { get; set; }
|
||||||
|
public float TransferRate { get; set; }
|
||||||
|
public float DockingDuration { get; set; }
|
||||||
|
public float UndockingDuration { get; set; }
|
||||||
|
public float UndockDistance { get; set; }
|
||||||
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using SpaceGame.Api.Universe.Bootstrap;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Universe.Simulation;
|
namespace SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
public sealed class WorldService(
|
public sealed class WorldService(
|
||||||
IWebHostEnvironment environment,
|
WorldBootstrapper worldBootstrapper,
|
||||||
IOptions<WorldGenerationOptions> worldGenerationOptions,
|
IOptions<BalanceOptions> balance,
|
||||||
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
||||||
{
|
{
|
||||||
private const int DeltaHistoryLimit = 256;
|
private const int DeltaHistoryLimit = 256;
|
||||||
|
|
||||||
private readonly Lock _sync = new();
|
private readonly Lock _sync = new();
|
||||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||||
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
|
private readonly WorldBootstrapper _bootstrapper = worldBootstrapper;
|
||||||
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
||||||
private readonly PlayerFactionService _playerFaction = new();
|
private readonly PlayerFactionService _playerFaction = new();
|
||||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||||
private readonly Queue<WorldDelta> _history = [];
|
private readonly Queue<WorldDelta> _history = [];
|
||||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
private SimulationWorld _world = worldBootstrapper.Bootstrap();
|
||||||
private long _sequence;
|
private long _sequence;
|
||||||
private BalanceDefinition? _balanceOverride;
|
private BalanceOptions? _balanceOverride;
|
||||||
|
|
||||||
public WorldSnapshot GetSnapshot()
|
public WorldSnapshot GetSnapshot()
|
||||||
{
|
{
|
||||||
@@ -45,32 +46,31 @@ public sealed class WorldService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BalanceDefinition GetBalance()
|
public BalanceOptions GetBalance()
|
||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
var b = _world.Balance;
|
return new BalanceOptions
|
||||||
return new BalanceDefinition
|
|
||||||
{
|
{
|
||||||
SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
|
SimulationSpeedMultiplier = balance.Value.SimulationSpeedMultiplier,
|
||||||
YPlane = b.YPlane,
|
YPlane = balance.Value.YPlane,
|
||||||
ArrivalThreshold = b.ArrivalThreshold,
|
ArrivalThreshold = balance.Value.ArrivalThreshold,
|
||||||
MiningRate = b.MiningRate,
|
MiningRate = balance.Value.MiningRate,
|
||||||
MiningCycleSeconds = b.MiningCycleSeconds,
|
MiningCycleSeconds = balance.Value.MiningCycleSeconds,
|
||||||
TransferRate = b.TransferRate,
|
TransferRate = balance.Value.TransferRate,
|
||||||
DockingDuration = b.DockingDuration,
|
DockingDuration = balance.Value.DockingDuration,
|
||||||
UndockingDuration = b.UndockingDuration,
|
UndockingDuration = balance.Value.UndockingDuration,
|
||||||
UndockDistance = b.UndockDistance,
|
UndockDistance = balance.Value.UndockDistance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
|
public BalanceOptions UpdateBalance(BalanceOptions balance)
|
||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_balanceOverride = SanitizeBalance(balance);
|
_balanceOverride = SanitizeBalance(balance);
|
||||||
ApplyBalance(_world, _balanceOverride);
|
ApplyBalance(_balanceOverride);
|
||||||
return GetBalance();
|
return GetBalance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,10 +285,10 @@ public sealed class WorldService(
|
|||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_world = _loader.Load();
|
_world = _bootstrapper.Bootstrap();
|
||||||
if (_balanceOverride is not null)
|
if (_balanceOverride is not null)
|
||||||
{
|
{
|
||||||
ApplyBalance(_world, _balanceOverride);
|
ApplyBalance(_balanceOverride);
|
||||||
}
|
}
|
||||||
_sequence += 1;
|
_sequence += 1;
|
||||||
_history.Clear();
|
_history.Clear();
|
||||||
@@ -323,26 +323,25 @@ public sealed class WorldService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
|
private void ApplyBalance(BalanceOptions value)
|
||||||
world.Balance = new BalanceDefinition
|
|
||||||
{
|
{
|
||||||
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
|
balance.Value.SimulationSpeedMultiplier = value.SimulationSpeedMultiplier;
|
||||||
YPlane = balance.YPlane,
|
balance.Value.YPlane = value.YPlane;
|
||||||
ArrivalThreshold = balance.ArrivalThreshold,
|
balance.Value.ArrivalThreshold = value.ArrivalThreshold;
|
||||||
MiningRate = balance.MiningRate,
|
balance.Value.MiningRate = value.MiningRate;
|
||||||
MiningCycleSeconds = balance.MiningCycleSeconds,
|
balance.Value.MiningCycleSeconds = value.MiningCycleSeconds;
|
||||||
TransferRate = balance.TransferRate,
|
balance.Value.TransferRate = value.TransferRate;
|
||||||
DockingDuration = balance.DockingDuration,
|
balance.Value.DockingDuration = value.DockingDuration;
|
||||||
UndockingDuration = balance.UndockingDuration,
|
balance.Value.UndockingDuration = value.UndockingDuration;
|
||||||
UndockDistance = balance.UndockDistance,
|
balance.Value.UndockDistance = value.UndockDistance;
|
||||||
};
|
}
|
||||||
|
|
||||||
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate)
|
private static BalanceOptions SanitizeBalance(BalanceOptions candidate)
|
||||||
{
|
{
|
||||||
static float finiteOr(float value, float fallback) =>
|
static float finiteOr(float value, float fallback) =>
|
||||||
float.IsFinite(value) ? value : fallback;
|
float.IsFinite(value) ? value : fallback;
|
||||||
|
|
||||||
return new BalanceDefinition
|
return new BalanceOptions
|
||||||
{
|
{
|
||||||
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||||
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||||
|
|||||||
@@ -11,6 +11,17 @@
|
|||||||
"AiControllerFactionCount": 0,
|
"AiControllerFactionCount": 0,
|
||||||
"GeneratePlayerFaction": false
|
"GeneratePlayerFaction": false
|
||||||
},
|
},
|
||||||
|
"Balance": {
|
||||||
|
"SimulationSpeedMultiplier": 1.5,
|
||||||
|
"YPlane": 4,
|
||||||
|
"ArrivalThreshold": 16,
|
||||||
|
"MiningRate": 10,
|
||||||
|
"MiningCycleSeconds": 10,
|
||||||
|
"TransferRate": 56,
|
||||||
|
"DockingDuration": 1.2,
|
||||||
|
"UndockingDuration": 1.2,
|
||||||
|
"UndockDistance": 42
|
||||||
|
},
|
||||||
"OrbitalSimulation": {
|
"OrbitalSimulation": {
|
||||||
"SimulatedSecondsPerRealSecond": 0
|
"SimulatedSecondsPerRealSecond": 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
"TargetSystemCount": 160,
|
"TargetSystemCount": 160,
|
||||||
"IncludeSolSystem": true
|
"IncludeSolSystem": true
|
||||||
},
|
},
|
||||||
|
"Balance": {
|
||||||
|
"SimulationSpeedMultiplier": 1.5,
|
||||||
|
"YPlane": 4,
|
||||||
|
"ArrivalThreshold": 16,
|
||||||
|
"MiningRate": 10,
|
||||||
|
"MiningCycleSeconds": 10,
|
||||||
|
"TransferRate": 56,
|
||||||
|
"DockingDuration": 1.2,
|
||||||
|
"UndockingDuration": 1.2,
|
||||||
|
"UndockDistance": 42
|
||||||
|
},
|
||||||
"OrbitalSimulation": {
|
"OrbitalSimulation": {
|
||||||
"SimulatedSecondsPerRealSecond": 0
|
"SimulatedSecondsPerRealSecond": 0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"simulationSpeedMultiplier": 1.5,
|
|
||||||
"yPlane": 4,
|
|
||||||
"arrivalThreshold": 16,
|
|
||||||
"miningRate": 10,
|
|
||||||
"miningCycleSeconds": 10,
|
|
||||||
"transferRate": 56,
|
|
||||||
"dockingDuration": 1.2,
|
|
||||||
"undockingDuration": 1.2,
|
|
||||||
"undockDistance": 42
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"gameStartOptions": {
|
||||||
|
"seed": 1,
|
||||||
|
"worldGeneration": {
|
||||||
|
"targetSystemCount": 2,
|
||||||
|
"aiControllerFactionCount": 0,
|
||||||
|
"generatePlayerFaction": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"initialStations": [
|
"initialStations": [
|
||||||
{
|
{
|
||||||
"label": "Dominion Power Relay",
|
"label": "Dominion Power Relay",
|
||||||
|
|||||||
Reference in New Issue
Block a user