331 lines
13 KiB
C#
331 lines
13 KiB
C#
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
|
|
|
using SpaceGame.Api.Universe.Scenario;
|
|
|
|
namespace SpaceGame.Api.Universe.Simulation;
|
|
|
|
internal sealed class OrbitalStateUpdater
|
|
{
|
|
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
|
|
|
internal OrbitalStateUpdater(OrbitalSimulationOptions orbitalSimulation)
|
|
{
|
|
_orbitalSimulation = orbitalSimulation;
|
|
}
|
|
|
|
private static Vector3 ComputePlanetPosition(PlanetDefinition planet, float timeSeconds)
|
|
{
|
|
var eccentricity = Math.Clamp(planet.OrbitEccentricity, 0f, 0.85f);
|
|
var meanAnomaly = DegreesToRadians(planet.OrbitPhaseAtEpoch) + (timeSeconds * planet.OrbitSpeed);
|
|
var eccentricAnomaly = meanAnomaly
|
|
+ (eccentricity * MathF.Sin(meanAnomaly))
|
|
+ (0.5f * eccentricity * eccentricity * MathF.Sin(2f * meanAnomaly));
|
|
var semiMajorAxis = SimulationUnits.AuToKilometers(planet.OrbitRadius);
|
|
var semiMinorAxis = semiMajorAxis * MathF.Sqrt(MathF.Max(1f - (eccentricity * eccentricity), 0.05f));
|
|
var local = new Vector3(
|
|
semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity),
|
|
0f,
|
|
semiMinorAxis * MathF.Sin(eccentricAnomaly));
|
|
|
|
local = RotateAroundY(local, DegreesToRadians(planet.OrbitArgumentOfPeriapsis));
|
|
local = RotateAroundX(local, DegreesToRadians(planet.OrbitInclination));
|
|
local = RotateAroundY(local, DegreesToRadians(planet.OrbitLongitudeOfAscendingNode));
|
|
return local;
|
|
}
|
|
|
|
private static Vector3 ComputeMoonOffset(MoonDefinition moon, float timeSeconds)
|
|
{
|
|
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed);
|
|
var local = new Vector3(
|
|
MathF.Cos(angle) * moon.OrbitRadius,
|
|
0f,
|
|
MathF.Sin(angle) * moon.OrbitRadius);
|
|
local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination));
|
|
local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode));
|
|
return local;
|
|
}
|
|
|
|
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
|
|
{
|
|
var baseSpeed = 0.24f;
|
|
return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f));
|
|
}
|
|
|
|
private static Vector3 ComputeResourceNodeOffset(ResourceNodeRuntime node, float timeSeconds)
|
|
{
|
|
var angle = node.OrbitPhase + (timeSeconds * ComputeResourceNodeOrbitSpeed(node));
|
|
var orbit = new Vector3(
|
|
MathF.Cos(angle) * node.OrbitRadius,
|
|
0f,
|
|
MathF.Sin(angle) * node.OrbitRadius);
|
|
return RotateAroundX(orbit, node.OrbitInclination);
|
|
}
|
|
|
|
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
|
|
Vector3 planetPosition,
|
|
PlanetDefinition planet)
|
|
{
|
|
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
|
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
|
var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
|
|
var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
|
|
var triangularAngle = MathF.PI / 3f;
|
|
|
|
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
|
|
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
|
|
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
|
|
yield return new LagrangePointPlacement(
|
|
"L4",
|
|
Add(
|
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
|
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
|
|
yield return new LagrangePointPlacement(
|
|
"L5",
|
|
Add(
|
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
|
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
|
|
}
|
|
|
|
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
|
|
{
|
|
var planetMassProxy = EstimatePlanetMassRatio(planet);
|
|
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
|
|
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
|
|
return MathF.Max(minimumOffset, hillLikeOffset);
|
|
}
|
|
|
|
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
|
|
{
|
|
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
|
|
var densityFactor = planet.PlanetType switch
|
|
{
|
|
"gas-giant" => 0.24f,
|
|
"ice-giant" => 0.18f,
|
|
"oceanic" => 0.95f,
|
|
"ice" => 0.7f,
|
|
_ => 1f,
|
|
};
|
|
|
|
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
|
|
return earthMasses / 332_946f;
|
|
}
|
|
|
|
private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback)
|
|
{
|
|
var length = MathF.Sqrt(value.LengthSquared());
|
|
if (length <= 0.0001f)
|
|
{
|
|
return fallback;
|
|
}
|
|
|
|
return value.Divide(length);
|
|
}
|
|
|
|
private static Vector3 RotateAroundX(Vector3 value, float angle)
|
|
{
|
|
var cos = MathF.Cos(angle);
|
|
var sin = MathF.Sin(angle);
|
|
return new Vector3(
|
|
value.X,
|
|
(value.Y * cos) - (value.Z * sin),
|
|
(value.Y * sin) + (value.Z * cos));
|
|
}
|
|
|
|
private static Vector3 RotateAroundY(Vector3 value, float angle)
|
|
{
|
|
var cos = MathF.Cos(angle);
|
|
var sin = MathF.Sin(angle);
|
|
return new Vector3(
|
|
(value.X * cos) + (value.Z * sin),
|
|
value.Y,
|
|
(-value.X * sin) + (value.Z * cos));
|
|
}
|
|
|
|
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
|
|
|
|
private static Vector3 Scale(Vector3 value, float scalar) => new(value.X * scalar, value.Y * scalar, value.Z * scalar);
|
|
|
|
private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
|
|
|
|
private static float HashUnit(string input)
|
|
{
|
|
unchecked
|
|
{
|
|
var hash = 2166136261u;
|
|
foreach (var character in input)
|
|
{
|
|
hash ^= character;
|
|
hash *= 16777619u;
|
|
}
|
|
|
|
return (hash & 0x00FFFFFF) / (float)0x01000000;
|
|
}
|
|
}
|
|
|
|
internal void Update(SimulationWorld world)
|
|
{
|
|
var worldTimeSeconds = (float)world.OrbitalTimeSeconds;
|
|
var celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal);
|
|
|
|
foreach (var system in world.Systems)
|
|
{
|
|
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
|
|
{
|
|
var star = system.Definition.Stars[starIndex];
|
|
var starNodeId = $"node-{system.Definition.Id}-star-{starIndex + 1}";
|
|
if (!celestialsById.TryGetValue(starNodeId, out var starNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (star.OrbitRadius <= 0f)
|
|
{
|
|
starNode.Position = Vector3.Zero;
|
|
}
|
|
else
|
|
{
|
|
var angle = DegreesToRadians(star.OrbitPhaseAtEpoch) + (worldTimeSeconds * star.OrbitSpeed);
|
|
starNode.Position = new Vector3(MathF.Cos(angle) * star.OrbitRadius, 0f, MathF.Sin(angle) * star.OrbitRadius);
|
|
}
|
|
}
|
|
|
|
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
|
|
{
|
|
var planet = system.Definition.Planets[planetIndex];
|
|
var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
|
|
if (!celestialsById.TryGetValue(planetNodeId, out var planetNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds);
|
|
planetNode.Position = planetPosition;
|
|
|
|
foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
|
{
|
|
var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}";
|
|
if (celestialsById.TryGetValue(lagrangeId, out var lagrangeNode))
|
|
{
|
|
lagrangeNode.Position = lagrange.Position;
|
|
}
|
|
}
|
|
|
|
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
|
|
{
|
|
var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
|
|
if (!celestialsById.TryGetValue(moonId, out var moonNode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet.Moons[moonIndex], worldTimeSeconds));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var station in world.Stations)
|
|
{
|
|
if (station.AnchorId is not null && world.Anchors.Any(candidate => candidate.Id == station.AnchorId))
|
|
{
|
|
station.Position = Vector3.Zero;
|
|
}
|
|
}
|
|
|
|
foreach (var node in world.Nodes)
|
|
{
|
|
node.Position = ComputeResourceNodeOffset(node, worldTimeSeconds);
|
|
}
|
|
|
|
var nodeAnchorsById = world.Nodes.ToDictionary(node => node.AnchorId, StringComparer.Ordinal);
|
|
foreach (var anchor in world.Anchors)
|
|
{
|
|
if (string.Equals(anchor.SourceEntityKind, "resource-node", StringComparison.Ordinal))
|
|
{
|
|
if (nodeAnchorsById.TryGetValue(anchor.Id, out var node))
|
|
{
|
|
if (anchor.ParentAnchorId is not null && celestialsById.TryGetValue(anchor.ParentAnchorId, out var anchorCelestial))
|
|
{
|
|
anchor.Position = Add(anchorCelestial.Position, node.Position);
|
|
}
|
|
else
|
|
{
|
|
anchor.Position = node.Position;
|
|
}
|
|
|
|
anchor.LocalSpaceRadius = node.LocalSpaceRadius;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (celestialsById.TryGetValue(anchor.Id, out var celestial))
|
|
{
|
|
anchor.Position = celestial.Position;
|
|
anchor.LocalSpaceRadius = celestial.LocalSpaceRadius;
|
|
anchor.ParentAnchorId = celestial.ParentAnchorId;
|
|
anchor.OccupyingStructureId = celestial.OccupyingStructureId;
|
|
anchor.OrbitReferenceId = celestial.OrbitReferenceId;
|
|
}
|
|
}
|
|
|
|
foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null))
|
|
{
|
|
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
if (station is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var dockedPosition = GetShipDockedPosition(ship, station);
|
|
ship.Position = dockedPosition;
|
|
ship.TargetPosition = dockedPosition;
|
|
}
|
|
}
|
|
|
|
internal void SyncSpatialState(SimulationWorld world)
|
|
{
|
|
foreach (var ship in world.Ships)
|
|
{
|
|
ship.SpatialState.CurrentSystemId = ship.SystemId;
|
|
ship.SpatialState.LocalPosition = ship.Position;
|
|
if (ship.SpatialState.Transit is not null)
|
|
{
|
|
ship.SpatialState.CurrentAnchorId = null;
|
|
continue;
|
|
}
|
|
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
|
var currentAnchor = ship.SpatialState.CurrentAnchorId is not null
|
|
? world.Anchors.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentAnchorId)
|
|
: null;
|
|
if (currentAnchor is null || !string.Equals(currentAnchor.SystemId, ship.SystemId, StringComparison.Ordinal))
|
|
{
|
|
currentAnchor = world.Anchors
|
|
.Where(candidate => candidate.SystemId == ship.SystemId)
|
|
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
|
|
var localSystemOffset = SimulationUnits.MetersToKilometers(ship.Position);
|
|
ship.SpatialState.SystemPosition = currentAnchor is null
|
|
? localSystemOffset
|
|
: Add(currentAnchor.Position, localSystemOffset);
|
|
|
|
if (ship.DockedStationId is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
if (station is not null)
|
|
{
|
|
ship.SpatialState.CurrentAnchorId = station.AnchorId;
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly record struct LagrangePointPlacement(string Designation, Vector3 Position);
|
|
}
|