improvement on gm windows, ai
This commit is contained in:
16
apps/backend/Universe/Api/GetBalanceHandler.cs
Normal file
16
apps/backend/Universe/Api/GetBalanceHandler.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using FastEndpoints;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Api;
|
||||
|
||||
public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/balance");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override Task HandleAsync(CancellationToken cancellationToken) =>
|
||||
SendOkAsync(worldService.GetBalance(), cancellationToken);
|
||||
}
|
||||
49
apps/backend/Universe/Api/GetTelemetryHandler.cs
Normal file
49
apps/backend/Universe/Api/GetTelemetryHandler.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using FastEndpoints;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Api;
|
||||
|
||||
public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService worldService) : EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/telemetry");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var status = worldService.GetStatus();
|
||||
var connections = worldService.GetConnectionStats();
|
||||
var uptime = telemetry.Uptime;
|
||||
|
||||
return SendOkAsync(new
|
||||
{
|
||||
process = new
|
||||
{
|
||||
uptimeSeconds = uptime.TotalSeconds,
|
||||
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
|
||||
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
|
||||
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
|
||||
threadCount = telemetry.ThreadCount,
|
||||
processorCount = Environment.ProcessorCount,
|
||||
},
|
||||
simulation = new
|
||||
{
|
||||
sequence = status.Sequence,
|
||||
connectedClients = connections.ConnectedClients,
|
||||
deltaHistoryCount = connections.DeltaHistoryCount,
|
||||
tickIntervalMs = 200,
|
||||
},
|
||||
runtime = new
|
||||
{
|
||||
frameworkDescription = RuntimeInformation.FrameworkDescription,
|
||||
osDescription = RuntimeInformation.OSDescription,
|
||||
gcGen0 = GC.CollectionCount(0),
|
||||
gcGen1 = GC.CollectionCount(1),
|
||||
gcGen2 = GC.CollectionCount(2),
|
||||
},
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
20
apps/backend/Universe/Api/UpdateBalanceHandler.cs
Normal file
20
apps/backend/Universe/Api/UpdateBalanceHandler.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using FastEndpoints;
|
||||
using SpaceGame.Api.Definitions;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Api;
|
||||
|
||||
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/balance");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
|
||||
{
|
||||
var applied = worldService.UpdateBalance(req);
|
||||
return SendOkAsync(applied, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ public sealed class SimulationWorld
|
||||
{
|
||||
public required string Label { get; init; }
|
||||
public required int Seed { get; init; }
|
||||
public required BalanceDefinition Balance { get; init; }
|
||||
public required BalanceDefinition Balance { get; set; }
|
||||
public required List<SystemRuntime> Systems { get; init; }
|
||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||
public required List<CelestialRuntime> Celestials { get; init; }
|
||||
|
||||
@@ -224,6 +224,23 @@ internal sealed class SpatialBuilder
|
||||
|
||||
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
|
||||
{
|
||||
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
|
||||
{
|
||||
var reference when reference.StartsWith("star-", StringComparison.Ordinal)
|
||||
=> $"node-{graph.SystemId}-{reference}",
|
||||
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
|
||||
=> $"node-{graph.SystemId}-{reference}",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (anchorId is not null)
|
||||
{
|
||||
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -9,7 +9,7 @@ internal sealed class SystemGenerationService
|
||||
|
||||
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||
authoredSystems
|
||||
.Select(CloneSystemDefinition)
|
||||
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||
.ToList();
|
||||
|
||||
internal List<SolarSystemDefinition> ExpandSystems(
|
||||
@@ -126,6 +126,7 @@ internal sealed class SystemGenerationService
|
||||
.Select(node => new ResourceNodeDefinition
|
||||
{
|
||||
SourceKind = node.SourceKind,
|
||||
AnchorReference = node.AnchorReference,
|
||||
Angle = node.Angle,
|
||||
RadiusOffset = node.RadiusOffset,
|
||||
InclinationDegrees = node.InclinationDegrees,
|
||||
@@ -137,7 +138,7 @@ internal sealed class SystemGenerationService
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new SolarSystemDefinition
|
||||
return EnsureStrategicResourceCoverage(new SolarSystemDefinition
|
||||
{
|
||||
Id = id,
|
||||
Label = label,
|
||||
@@ -161,7 +162,7 @@ internal sealed class SystemGenerationService
|
||||
},
|
||||
ResourceNodes = resourceNodes,
|
||||
Planets = planets,
|
||||
};
|
||||
}, generatedIndex + 1024);
|
||||
}
|
||||
|
||||
private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
|
||||
@@ -182,6 +183,7 @@ internal sealed class SystemGenerationService
|
||||
ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
|
||||
{
|
||||
SourceKind = node.SourceKind,
|
||||
AnchorReference = node.AnchorReference,
|
||||
Angle = node.Angle,
|
||||
RadiusOffset = node.RadiusOffset,
|
||||
InclinationDegrees = node.InclinationDegrees,
|
||||
@@ -223,6 +225,7 @@ internal sealed class SystemGenerationService
|
||||
nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
|
||||
{
|
||||
SourceKind = node.SourceKind,
|
||||
AnchorReference = node.AnchorReference,
|
||||
Angle = node.Angle,
|
||||
RadiusOffset = node.RadiusOffset,
|
||||
InclinationDegrees = node.InclinationDegrees,
|
||||
@@ -234,10 +237,30 @@ internal sealed class SystemGenerationService
|
||||
}));
|
||||
}
|
||||
|
||||
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static SolarSystemDefinition EnsureStrategicResourceCoverage(SolarSystemDefinition system, int seed)
|
||||
{
|
||||
for (var index = 0; index < system.ResourceNodes.Count; index += 1)
|
||||
{
|
||||
system.ResourceNodes[index] = SanitizeResourceNode(system.ResourceNodes[index], system.Planets, seed, index);
|
||||
}
|
||||
|
||||
var requiredItems = new[] { "ore", "silicon", "ice", "hydrogen", "helium", "methane" };
|
||||
foreach (var itemId in requiredItems)
|
||||
{
|
||||
if (system.ResourceNodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
system.ResourceNodes.Add(BuildStrategicResourceNode(itemId, system.Planets, seed, system.ResourceNodes.Count));
|
||||
}
|
||||
|
||||
return system;
|
||||
}
|
||||
|
||||
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
||||
{
|
||||
var allPositions = occupiedPositions.ToList();
|
||||
@@ -303,25 +326,124 @@ internal sealed class SystemGenerationService
|
||||
return $"gen-{ordinal}-{slug}";
|
||||
}
|
||||
|
||||
private static IEnumerable<ResourceNodeDefinition> BuildAsteroidBeltNodes(int generatedIndex, IReadOnlyList<PlanetDefinition> planets)
|
||||
private static ResourceNodeDefinition BuildStrategicResourceNode(
|
||||
string itemId,
|
||||
IReadOnlyList<PlanetDefinition> planets,
|
||||
int seed,
|
||||
int ordinal)
|
||||
{
|
||||
var nodeCount = 4 + (generatedIndex % 4);
|
||||
var oreAmount = 1000f;
|
||||
|
||||
for (var index = 0; index < nodeCount; index += 1)
|
||||
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
|
||||
return new ResourceNodeDefinition
|
||||
{
|
||||
yield return new ResourceNodeDefinition
|
||||
SourceKind = "local-space",
|
||||
AnchorReference = ResolveStrategicAnchorReference(itemId, planets, ordinal),
|
||||
Angle = (MathF.PI * 2f * ((ordinal % 7) / 7f)) + Jitter(seed, 400 + ordinal, 0.35f),
|
||||
RadiusOffset = 150000f + Jitter(seed, 460 + ordinal, 42000f),
|
||||
InclinationDegrees = Jitter(seed, 520 + ordinal, 10f),
|
||||
AnchorPlanetIndex = anchorPlanetIndex,
|
||||
OreAmount = itemId switch
|
||||
{
|
||||
SourceKind = "asteroid-belt",
|
||||
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
|
||||
RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f),
|
||||
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
|
||||
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
|
||||
OreAmount = oreAmount,
|
||||
ItemId = "ore",
|
||||
ShardCount = 6 + (index % 4),
|
||||
};
|
||||
"ore" => 12000f,
|
||||
"silicon" => 10000f,
|
||||
"ice" => 9000f,
|
||||
_ => 8000f,
|
||||
},
|
||||
ItemId = itemId,
|
||||
ShardCount = itemId switch
|
||||
{
|
||||
"ore" or "silicon" or "ice" => 8,
|
||||
_ => 6,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static ResourceNodeDefinition SanitizeResourceNode(
|
||||
ResourceNodeDefinition node,
|
||||
IReadOnlyList<PlanetDefinition> planets,
|
||||
int seed,
|
||||
int ordinal)
|
||||
{
|
||||
node.SourceKind = "local-space";
|
||||
node.AnchorReference ??= ResolveLegacyAnchorReference(node, planets, seed, ordinal);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static string ResolveLegacyAnchorReference(
|
||||
ResourceNodeDefinition node,
|
||||
IReadOnlyList<PlanetDefinition> planets,
|
||||
int seed,
|
||||
int ordinal)
|
||||
{
|
||||
if (node.AnchorMoonIndex is int moonIndex && node.AnchorPlanetIndex is int planetIndex && planetIndex >= 0)
|
||||
{
|
||||
return $"planet-{planetIndex + 1}-moon-{moonIndex + 1}";
|
||||
}
|
||||
|
||||
if (node.AnchorPlanetIndex is int anchoredPlanetIndex && anchoredPlanetIndex >= 0)
|
||||
{
|
||||
return $"planet-{anchoredPlanetIndex + 1}";
|
||||
}
|
||||
|
||||
return ResolveStrategicAnchorReference(node.ItemId, planets, ordinal + seed);
|
||||
}
|
||||
|
||||
private static string ResolveStrategicAnchorReference(string itemId, IReadOnlyList<PlanetDefinition> planets, int ordinal)
|
||||
{
|
||||
if (itemId is "hydrogen" or "helium" or "methane")
|
||||
{
|
||||
var gasGiantIndex = planets
|
||||
.Select((planet, index) => (planet, index))
|
||||
.FirstOrDefault(entry => entry.planet.PlanetType is "gas-giant" or "ice-giant")
|
||||
.index;
|
||||
return gasGiantIndex > 0 || (planets.Count > 0 && planets[0].PlanetType is "gas-giant" or "ice-giant")
|
||||
? $"planet-{gasGiantIndex + 1}"
|
||||
: "star-1";
|
||||
}
|
||||
|
||||
if (itemId == "ice")
|
||||
{
|
||||
var moonAnchor = planets
|
||||
.Select((planet, index) => (planet, index))
|
||||
.FirstOrDefault(entry => entry.planet.Moons.Count > 0 && entry.planet.PlanetType is "ice" or "ice-giant" or "oceanic");
|
||||
if (moonAnchor.planet is not null && moonAnchor.planet.Moons.Count > 0)
|
||||
{
|
||||
return $"planet-{moonAnchor.index + 1}-moon-1";
|
||||
}
|
||||
}
|
||||
|
||||
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
|
||||
var lagrange = (ordinal % 3) switch
|
||||
{
|
||||
0 => "l1",
|
||||
1 => "l4",
|
||||
_ => "l5",
|
||||
};
|
||||
return $"planet-{anchorPlanetIndex + 1}-{lagrange}";
|
||||
}
|
||||
|
||||
private static int ResolveStrategicResourceAnchorPlanetIndex(string itemId, IReadOnlyList<PlanetDefinition> planets)
|
||||
{
|
||||
if (planets.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool MatchesPlanetType(PlanetDefinition planet) => itemId switch
|
||||
{
|
||||
"hydrogen" or "helium" or "methane" => planet.PlanetType is "gas-giant" or "ice-giant",
|
||||
"ice" => planet.PlanetType is "ice" or "ice-giant" or "oceanic",
|
||||
_ => planet.PlanetType is not "gas-giant" and not "ice-giant",
|
||||
};
|
||||
|
||||
for (var index = 0; index < planets.Count; index += 1)
|
||||
{
|
||||
if (MatchesPlanetType(planets[index]))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return ResolveAsteroidAnchorPlanetIndex(planets);
|
||||
}
|
||||
|
||||
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
|
||||
|
||||
@@ -34,7 +34,8 @@ internal sealed class WorldBuilder(
|
||||
systemsById,
|
||||
spatialLayout.SystemGraphs,
|
||||
spatialLayout.Celestials,
|
||||
catalog.ModuleDefinitions);
|
||||
catalog.ModuleDefinitions,
|
||||
catalog.ItemDefinitions);
|
||||
|
||||
seedingService.InitializeStationStockpiles(stations);
|
||||
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
||||
@@ -106,7 +107,8 @@ internal sealed class WorldBuilder(
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
var stationIdCounter = 0;
|
||||
@@ -136,9 +138,7 @@ internal sealed class WorldBuilder(
|
||||
stations.Add(station);
|
||||
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
||||
|
||||
var startingModules = plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
|
||||
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
|
||||
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
@@ -149,6 +149,91 @@ internal sealed class WorldBuilder(
|
||||
return stations;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildStartingModules(
|
||||
InitialStationDefinition plan,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var startingModules = new List<string>(plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
|
||||
|
||||
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
|
||||
|
||||
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
|
||||
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||
{
|
||||
EnsureStartingModule(startingModules, objectiveModuleId);
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
||||
{
|
||||
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
|
||||
{
|
||||
EnsureStartingModule(startingModules, storageModuleId);
|
||||
}
|
||||
}
|
||||
|
||||
return startingModules;
|
||||
}
|
||||
|
||||
private static string? GetObjectiveStartingModuleId(string? objective) =>
|
||||
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_ref_ore_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static IEnumerable<string> GetRequiredStartingStorageModules(
|
||||
string moduleId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.Production
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.Products)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var storageModuleId = itemDefinition.CargoKind switch
|
||||
{
|
||||
"solid" => "module_arg_stor_solid_m_01",
|
||||
"liquid" => "module_arg_stor_liquid_m_01",
|
||||
_ => "module_arg_stor_container_m_01",
|
||||
};
|
||||
|
||||
yield return storageModuleId;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureStartingModule(List<string> modules, string moduleId)
|
||||
{
|
||||
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById)
|
||||
|
||||
@@ -65,33 +65,6 @@ internal sealed class WorldSeedingService
|
||||
foreach (var station in stations)
|
||||
{
|
||||
InitializeStationPopulation(station);
|
||||
if (station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
||||
{
|
||||
station.Inventory["energycells"] = MathF.Max(GetInventoryAmount(station.Inventory, "energycells"), 240f);
|
||||
}
|
||||
|
||||
if (station.InstalledModules.Contains("module_gen_prod_refinedmetals_01", StringComparer.Ordinal))
|
||||
{
|
||||
station.Inventory["ore"] = MathF.Max(GetInventoryAmount(station.Inventory, "ore"), 220f);
|
||||
}
|
||||
|
||||
if (station.InstalledModules.Contains("module_gen_prod_hullparts_01", StringComparer.Ordinal))
|
||||
{
|
||||
station.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(station.Inventory, "refinedmetals"), 240f);
|
||||
station.Inventory["graphene"] = MathF.Max(GetInventoryAmount(station.Inventory, "graphene"), 80f);
|
||||
}
|
||||
|
||||
if (station.InstalledModules.Contains("module_gen_prod_claytronics_01", StringComparer.Ordinal))
|
||||
{
|
||||
station.Inventory["antimattercells"] = MathF.Max(GetInventoryAmount(station.Inventory, "antimattercells"), 90f);
|
||||
station.Inventory["microchips"] = MathF.Max(GetInventoryAmount(station.Inventory, "microchips"), 120f);
|
||||
station.Inventory["quantumtubes"] = MathF.Max(GetInventoryAmount(station.Inventory, "quantumtubes"), 90f);
|
||||
}
|
||||
|
||||
if (station.Population > 0f)
|
||||
{
|
||||
station.Inventory["water"] = MathF.Max(60f, station.Population * 1.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +118,11 @@ internal sealed class WorldSeedingService
|
||||
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
if (HasSatisfiedStarterObjectiveLayout(world, station))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
|
||||
if (moduleId is null || station.CelestialId is null)
|
||||
{
|
||||
@@ -200,6 +178,78 @@ internal sealed class WorldSeedingService
|
||||
return (sites, orders);
|
||||
}
|
||||
|
||||
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var role = StationSimulationService.DetermineStationRole(station);
|
||||
var objectiveModuleId = role switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_prod_refinedmetals_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (objectiveModuleId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
|
||||
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
|
||||
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
|
||||
{
|
||||
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
|
||||
{
|
||||
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.Production
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.Products)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return itemDefinition.CargoKind switch
|
||||
{
|
||||
"solid" => "module_arg_stor_solid_m_01",
|
||||
"liquid" => "module_arg_stor_liquid_m_01",
|
||||
_ => "module_arg_stor_container_m_01",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
||||
{
|
||||
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||
@@ -385,6 +435,13 @@ internal sealed class WorldSeedingService
|
||||
Color = "#ff8f70",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
"nadir-syndicate" => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Nadir Syndicate",
|
||||
Color = "#91e6a8",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
_ => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
|
||||
44
apps/backend/Universe/Simulation/TelemetryService.cs
Normal file
44
apps/backend/Universe/Simulation/TelemetryService.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class TelemetryService : IDisposable
|
||||
{
|
||||
private readonly Process _process = Process.GetCurrentProcess();
|
||||
private readonly Timer _timer;
|
||||
private double _cpuPercent;
|
||||
private DateTime _lastSampleTime;
|
||||
private TimeSpan _lastCpuTime;
|
||||
|
||||
public TelemetryService()
|
||||
{
|
||||
_process.Refresh();
|
||||
_lastSampleTime = DateTime.UtcNow;
|
||||
_lastCpuTime = _process.TotalProcessorTime;
|
||||
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private void Sample(object? _)
|
||||
{
|
||||
_process.Refresh();
|
||||
var now = DateTime.UtcNow;
|
||||
var cpu = _process.TotalProcessorTime;
|
||||
var elapsed = (now - _lastSampleTime).TotalSeconds;
|
||||
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
|
||||
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
|
||||
_lastSampleTime = now;
|
||||
_lastCpuTime = cpu;
|
||||
}
|
||||
|
||||
public double CpuPercent => Volatile.Read(ref _cpuPercent);
|
||||
public long WorkingSetBytes => _process.WorkingSet64;
|
||||
public long GcMemoryBytes => GC.GetTotalMemory(false);
|
||||
public int ThreadCount => _process.Threads.Count;
|
||||
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Dispose();
|
||||
_process.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public sealed class WorldService(
|
||||
private readonly Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
||||
private long _sequence;
|
||||
private BalanceDefinition? _balanceOverride;
|
||||
|
||||
public WorldSnapshot GetSnapshot()
|
||||
{
|
||||
@@ -35,6 +36,44 @@ public sealed class WorldService(
|
||||
}
|
||||
}
|
||||
|
||||
public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return (_subscribers.Count, _history.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceDefinition GetBalance()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var b = _world.Balance;
|
||||
return new BalanceDefinition
|
||||
{
|
||||
SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
|
||||
YPlane = b.YPlane,
|
||||
ArrivalThreshold = b.ArrivalThreshold,
|
||||
MiningRate = b.MiningRate,
|
||||
MiningCycleSeconds = b.MiningCycleSeconds,
|
||||
TransferRate = b.TransferRate,
|
||||
DockingDuration = b.DockingDuration,
|
||||
UndockingDuration = b.UndockingDuration,
|
||||
UndockDistance = b.UndockDistance,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_balanceOverride = SanitizeBalance(balance);
|
||||
ApplyBalance(_world, _balanceOverride);
|
||||
return GetBalance();
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||
@@ -96,6 +135,10 @@ public sealed class WorldService(
|
||||
lock (_sync)
|
||||
{
|
||||
_world = _loader.Load();
|
||||
if (_balanceOverride is not null)
|
||||
{
|
||||
ApplyBalance(_world, _balanceOverride);
|
||||
}
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
@@ -127,6 +170,39 @@ public sealed class WorldService(
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
|
||||
world.Balance = new BalanceDefinition
|
||||
{
|
||||
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
|
||||
YPlane = balance.YPlane,
|
||||
ArrivalThreshold = balance.ArrivalThreshold,
|
||||
MiningRate = balance.MiningRate,
|
||||
MiningCycleSeconds = balance.MiningCycleSeconds,
|
||||
TransferRate = balance.TransferRate,
|
||||
DockingDuration = balance.DockingDuration,
|
||||
UndockingDuration = balance.UndockingDuration,
|
||||
UndockDistance = balance.UndockDistance,
|
||||
};
|
||||
|
||||
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate)
|
||||
{
|
||||
static float finiteOr(float value, float fallback) =>
|
||||
float.IsFinite(value) ? value : fallback;
|
||||
|
||||
return new BalanceDefinition
|
||||
{
|
||||
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
|
||||
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
|
||||
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
|
||||
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
|
||||
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
|
||||
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
|
||||
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||
delta.RequiresSnapshotRefresh
|
||||
|| delta.Events.Count > 0
|
||||
|
||||
Reference in New Issue
Block a user