tidssystem

This commit is contained in:
2026-11-01 16:52:32 +01:00
parent 9d44d14cc9
commit a7207086b2
24 changed files with 849 additions and 174 deletions

View File

@@ -0,0 +1,194 @@
using System.Numerics;
using Universe.App.Graphics;
using Universe.App.Runtime;
using Universe.Core.Configuration;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.Physics;
using Universe.Core.World;
namespace Universe.Tests;
public sealed class CelestialSimulationTests
{
private static readonly Lazy<StarSystemData> Sol = new(() => KnownSolarSystemCatalog.Load(
Path.Combine(AppContext.BaseDirectory, "KnownSolarSystem", "solar-systems.json")).GetById("sol"));
[Fact]
public void ClockRunsAtRealTimePausesAndAcceleratesWithoutDroppingSlowFrames()
{
var clock = new SimulationClock(new());
Assert.Equal(1, clock.TimeScale);
clock.Advance(2.5);
Assert.Equal(2.5, clock.SecondsSinceJ2000);
clock.TimeScale = 0; clock.Advance(120);
Assert.Equal(2.5, clock.ElapsedSeconds);
clock.TimeScale = 100; clock.Advance(60);
Assert.Equal(6002.5, clock.ElapsedSeconds);
Assert.Equal(SimulationClock.J2000.AddSeconds(6002.5), clock.Utc);
Assert.Throws<InvalidDataException>(() => new SimulationClock(new() { TimeScale = 101 }));
Assert.Throws<ArgumentOutOfRangeException>(() => clock.Advance(double.NaN));
}
[Theory]
[InlineData(0)]
[InlineData(.20563593)]
[InlineData(.9999)]
public void EllipseHasCorrectApsesAndClosesAfterAnOrbit(double eccentricity)
{
const double axis = 1e8, period = 100000;
var orbit = new KeplerOrbit(axis, eccentricity, 0, period, new(1, 0, 0), new(0, 0, 1));
Assert.InRange(System.Math.Abs(orbit.Position(0).Length - axis * (1 - eccentricity)), 0, 1e-6);
Assert.InRange(System.Math.Abs(orbit.Position(period * .5).Length - axis * (1 + eccentricity)), 0, 1e-6);
Assert.InRange((orbit.Position(1200) - orbit.Position(period * 1000000 + 1200)).Length, 0, 1e-5);
Assert.InRange((orbit.Position(-1200) - orbit.Position(period - 1200)).Length, 0, 1e-5);
}
[Fact]
public void MercuryAcceleratesAtPerihelionAndConservesKeplerEnergyAndAngularMomentum()
{
var orbit = Sol.Value.Planets.First(p => p.Id == "mercury").Orbit! with { MeanAnomalyAtEpochRadians = 0 };
DVector3 Velocity(double t) => (orbit.Position(t + 1) - orbit.Position(t - 1)) * .5;
var periSpeed = Velocity(0).Length;
var apSpeed = Velocity(orbit.PeriodSeconds * .5).Length;
Assert.InRange(periSpeed / apSpeed, 1.517, 1.519);
var mu = System.Math.Pow(System.Math.Tau / orbit.PeriodSeconds, 2) * System.Math.Pow(orbit.SemiMajorAxisMeters, 3);
var energy = -mu / (2 * orbit.SemiMajorAxisMeters);
var h = DVector3.Cross(orbit.Position(0), Velocity(0)).Length;
foreach (var fraction in new[] { .01, .17, .4, .8 })
{
var t = fraction * orbit.PeriodSeconds;
var v = Velocity(t); var r = orbit.Position(t);
Assert.InRange(System.Math.Abs((v.LengthSquared * .5 - mu / r.Length) / energy - 1), 0, 1e-7);
Assert.InRange(System.Math.Abs(DVector3.Cross(r, v).Length / h - 1), 0, 1e-7);
}
}
[Fact]
public void EarthUsesSiderealDayAndYearAndMoonOrbitsTheMovingEarth()
{
var start = CelestialSimulation.Evaluate(Sol.Value, 0);
var hour = CelestialSimulation.Evaluate(Sol.Value, 3600);
var earth = start.Planets.First(p => p.Id == "earth");
var later = hour.Planets[earth.Index];
Assert.InRange(earth.Rotation!.PeriodSeconds, 86164, 86165);
Assert.InRange(earth.Orbit!.PeriodSeconds / 86400, 365.25, 365.26);
Assert.InRange((later.PositionMeters - earth.PositionMeters).Length, 106e6, 110e6);
var moon = start.Planets.First(p => p.Id == "moon");
var day = CelestialSimulation.Evaluate(Sol.Value, 86400);
var relative = day.Planets[moon.Index].PositionMeters - day.Planets[earth.Index].PositionMeters;
Assert.InRange(relative.Length, 363e6, 406e6);
Assert.InRange((relative - moon.Orbit!.Position(86400)).Length, 0, .0001);
foreach (var p in start.Planets)
{
Assert.NotNull(p.Orbit); Assert.NotNull(p.Rotation);
Assert.Same(p.Terrain, hour.Planets[p.Index].Terrain);
}
}
[Theory]
[InlineData("earth", true)]
[InlineData("venus", false)]
[InlineData("uranus", false)]
public void SpinDirectionMatchesProgradeOrRetrogradeWithoutDoubleNegation(string id, bool prograde)
{
var p = Sol.Value.Planets.First(p => p.Id == id);
var a = p.Rotation!.At(0).ToWorld(new(1, 0, 0));
var b = p.Rotation.At(System.Math.Abs(p.Rotation.PeriodSeconds) / 100).ToWorld(new(1, 0, 0));
var orbit = p.Orbit!;
var angular = DVector3.Dot(DVector3.Cross(a, b), DVector3.Cross(orbit.U, orbit.V));
Assert.Equal(prograde, angular > 0);
}
[Fact]
public void StreamedSystemUsesSameTimeAfterUnloadingAndDoesNotAccumulateOrbitError()
{
var generator = new UniverseGenerator(42);
var streamer = new UniverseStreamer(generator) { SimulationSeconds = 1234567 };
var first = streamer.Load(new(0, 0, 0));
streamer.Load(new(1000, 0, 0));
var again = streamer.Load(new(0, 0, 0));
Assert.Equal(first.ActiveSystem.Planets.Select(p => p.PositionMeters), again.ActiveSystem.Planets.Select(p => p.PositionMeters));
var refreshed = streamer.Refresh(again);
Assert.Equal(again.ActiveSystem.Planets.Select(p => p.PositionMeters), refreshed.ActiveSystem.Planets.Select(p => p.PositionMeters));
streamer.SimulationSeconds += 1000;
var moved = streamer.Refresh(again);
Assert.NotEqual(again.ActiveSystem.Planets[0].PositionMeters, moved.ActiveSystem.Planets[0].PositionMeters);
}
[Fact]
public void GroundedPlayerFollowsOrbitAndTerrainRotationWithoutLosingClearance()
{
const double cell = 10000000;
var planet = new PlanetData(0, "Test", DVector3.Zero, 10000, 150, 0, Vector3.One, Vector3.Zero, 1e6, 0, 0, 20)
{
Orbit = new(1e6, .1, 0, 86400, new(1, 0, 0), new(0, 0, 1)),
Rotation = new(1000, 23.4, 30)
};
var basis = new StarSystemData(new(0, 0, 0), 1, [new(0, "Sun", DVector3.Zero, 1000, 1, Vector3.One, 1)], [planet]);
var before = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, 0), []);
var p = before.ActiveSystem.Planets[0];
var center = UniversePosition.AtCellCenter(basis.Coordinate, cell);
var direction = p.Orientation.ToWorld(new DVector3(.6, .8, 0));
var body = center.Add(p.PositionMeters, cell);
var player = new PlayerController(body.Add(direction * (TerrainHeight.Sample(p, direction) + 1.7), cell),
p.Orientation.ToWorld(new(0, 0, 1)), direction, cell, before);
player.Update(.01, 0, 0, false, false, before);
Assert.True(player.Grounded);
var local = p.Orientation.ToBody(player.Position.RelativeTo(body, cell));
for (var i = 1; i <= 20; i++)
{
var current = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, i * 10), []);
player.ApplyCelestialMotion(before, current);
p = current.ActiveSystem.Planets[0];
var offset = player.Position.RelativeTo(center.Add(p.PositionMeters, cell), cell);
Assert.InRange((p.Orientation.ToBody(offset) - local).Length, 0, .0001);
Assert.InRange(offset.Length - TerrainHeight.Sample(p, offset.Normalized()), 1.6999, 1.7001);
Assert.InRange(DVector3.Dot(player.Up, offset.Normalized()), .999999, 1.000001);
before = current;
}
player.Update(.01, 0, 0, true, false, before);
Assert.False(player.Grounded);
var jumpBody = center.Add(before.ActiveSystem.Planets[0].PositionMeters, cell);
var jumpLocal = before.ActiveSystem.Planets[0].Orientation.ToBody(player.Position.RelativeTo(jumpBody, cell));
var whileJumping = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, 210), []);
player.ApplyCelestialMotion(before, whileJumping);
p = whileJumping.ActiveSystem.Planets[0];
var carriedJump = p.Orientation.ToBody(player.Position.RelativeTo(center.Add(p.PositionMeters, cell), cell));
Assert.InRange((carriedJump - jumpLocal).Length, 0, .0001);
}
[Fact]
public void DebugTimeSliderSupportsPauseRealTimeAndMaximum()
{
Assert.Equal(0d, PauseMenu.TimeScaleHit(1010, 390, 1440, 900));
Assert.Equal(100d, PauseMenu.TimeScaleHit(1330, 390, 1440, 900));
Assert.Equal(1d, PauseMenu.TimeScaleHit(1129, 469, 1440, 900));
Assert.Null(PauseMenu.TimeScaleHit(720, 450, 1440, 900));
}
[Fact]
public void FocusFlightTracksMovingEarthAtMaximumTimeScale()
{
var cell = UniverseScale.DefaultCellSizeMeters;
var world = new UniverseNeighborhood(CelestialSimulation.Evaluate(Sol.Value, 0), []);
var camera = new FlyCamera(UniversePosition.AtCellCenter(new(0, 0, 0), cell), cell);
camera.SetInitialView(world.ActiveSystem, new StartupConfiguration { PlanetId = "earth", DistanceInRadii = 5 });
camera.RequestFocus([world.ActiveSystem], "earth");
var time = 0d;
for (var i = 0; i < 60 && camera.IsFocusing; i++)
{
time += 10;
var next = new UniverseNeighborhood(CelestialSimulation.Evaluate(Sol.Value, time), []);
camera.ApplyCelestialMotion(world, next, time);
camera.Update(.1, DVector3.Zero, next);
world = next;
}
Assert.False(camera.IsFocusing);
var earth = world.ActiveSystem.Planets.Single(p => p.Id == "earth");
var body = UniversePosition.AtCellCenter(new(0, 0, 0), cell).Add(earth.PositionMeters, cell);
var toward = body.RelativeTo(camera.Position, cell);
Assert.InRange(toward.Length / earth.RadiusMeters, 1.1, 10);
Assert.InRange(DVector3.Dot(toward.Normalized(), camera.Forward), .999, 1.001);
}
}