ArgumentException "Cannot construct a UniverseTime from NaN" in Vehicle.UpdatePerFrameData when eccentricity is just below 1 (0.999999 to 1)

Maxi

Member
Messages
197
Reaction score
146
(post mostly written by claude, verified be me)

Game Version
v2026.11.10.5801 (reproduced with stock parts only); first found on v2026.10.25.5707

Operating System
Windows 11

What Happened
When the eccentricity of a vehicle orbit lies in the band 1 - 1e-6 <= e < 1, the game throws an exception in the frame update. Stack on v2026.11.10.5801:
Code
System.ArgumentException: Cannot construct a UniverseTime from NaN
   at KSA.UniverseTime.NanosecondsFromSeconds(Double seconds)
   at KSA.UniverseTime.op_Subtraction(UniverseTime a, Double b)
   at KSA.Orbit.GetTimeFromToTrueAnomalyAtGameTime(TrueAnomaly tA, UniverseTime gameTime, Boolean suppressWarnings)
   at KSA.Orbit.TimeOfTrueAnomaly(TrueAnomaly tA, UniverseTime gameTime, Boolean suppressWarnings)
   at KSA.Orbit.GetNextPeriapsisTime(UniverseTime gameTime)
   at KSA.Vehicle.UpdatePerFrameData()
   at KSA.IParentBody.UpdatePerFrameDataTree()
   at KSA.IParentBody.UpdatePerFrameDataTree()
   at KSA.CelestialSystem.UpdatePerFrameData()
   at KSA.Universe.ApplyVehicleSolvers()
In the played game the next frames are Program.PrepareFrame and Program.OnFrame. On v2026.10.25.5707 the stack was the same, without the two IParentBody.UpdatePerFrameDataTree frames.

I first saw it in an automated run of a lunar capture burn. The probe came in on a hyperbolic orbit (e = 1.005 at t = 858 s), and the burn pushed it toward a bound orbit. In the next steps the eccentricity landed in the band just below 1, and the exception was thrown. It happened on every run with the same approach speed and never with a slightly different one, because the band is narrow. Per crossing the chance to hit it is small (one physics step of this burn changes e by about 1e-4, and the band is 1e-6 wide). So it will be rare but real for a player who does a long capture burn or slowly circularises an escape orbit.

Reproduction on v2026.11.10.5801
A stock probe core (CoreCommandC_Prefab_ProbeCoreSmallA) in a 300 km circular Earth orbit. Then its orbit was set the same way the Set Orbit debug window does it (Vehicle.DrawSetOrbitDebugWindow: new OrbitData(timeAtPeriapsis, parent.Mass, periapsis, eccentricity, inclination, node, argument), then Orbit.Create without state vectors, then a TeleportInputData in InputEvents.TeleportInputBuffer, which Vehicle.Teleport applies). Periapsis 300 km above the atmosphere, inclination, node and argument 0. After that the universe was stepped at 0.02 s per frame:
  • e = 0.99999 (control): Orbit.Elliptical, period 1.78e11 s, no exception in 50 frames.
  • e = 1.0000005 (control): Orbit.Parabolic, Period NaN, Eccentricity < 1.0 false, no exception in 50 frames.
  • e = 0.9999995: OrbitData gives SemiMajorAxis +Infinity, Period NaN and Apoapsis NaN, Orbit.Create gives Orbit.Parabolic with Eccentricity 0.9999995 (Eccentricity < 1.0 true, IsBound() false). The exception above is thrown in the first frame after the teleport. A direct call of Orbit.GetNextPeriapsisTime on this orbit throws the same exception.
The run used stock parts only and the normal game code, headless (my test harness and a small test plugin, no content mods). Headless, Vehicle.Teleport cannot run its last line Program.Instance.SchedulePlanetUpdate, because there is no renderer. That line schedules a planet update and comes after the orbit change.

What Was Expected
An orbit with e very close to 1 is handled as a parabolic orbit (no period, the next periapsis time is the periapsis time), and no exception is thrown.

Reproduction Steps
  1. Set Orbit debug tool: fly any vehicle, open Debug > Show Set Orbit Debug, choose Earth as Destination Body, leave Height Options on Periapsis and Offset on Above Atmosphere, enter 300000 as Height, enter 0.9999995 as Eccentricity, and click Set Orbit. The exception is expected in the next frame.
  2. Capture burn that crosses e = 1 slowly from the hyperbolic side: fly a vehicle on a hyperbolic orbit around a body and burn retrograde at periapsis with a low thrust or at a low time warp, so that e falls from above 1 to below 1 in many small steps. The exception appears when one step ends with 1 - 1e-6 <= e < 1.
Reproduction Rate
Always for e = 0.9999995 with the Set Orbit path. For a burn it is always for a given trajectory in a deterministic run, and in normal play it depends on a step that ends in the band, which is 1e-6 wide.

Additional Info
Found with mods loaded, while I tested my own part mods with automated flight tests, and reproduced on v2026.11.10.5801 with stock parts only (see above). Related reports for other NaN paths near e = 1: "Crash when orbit is near to eccentricity 1" (thread 983, Orbit.CreateFromStateCci with e slightly above 1, marked as fixed in June) and "Crash while moving BURN panel in map view, UniverseTime from NaN, Burn.PlusClicked" (thread 1280). The stack of this report is different from both.

The exception is not caught on this path: there is no try/catch around Universe.ApplyVehicleSolvers in Program.PrepareFrame or around PrepareFrame in Program.OnFrame, and other reports with the same top-level frames (for example thread 983) show "Unhandled exception" and the game ending. So a played game would stop, not only log a warning.

Technical analysis (decompiled 2026.10.25.5707, unchanged in 2026.11.10.5801: Orbit.cs and OrbitData.cs did not change)
Two different tests decide what an orbit is:
  • OrbitData.IsParabolic() (and Orbit.IsParabolic(double eccentricity)) test Math.Abs(Eccentricity - 1.0) <= 1E-06. In the OrbitData constructor (elements version) the parabolic case sets SemiMajorAxis = double.PositiveInfinity, Period = double.NaN and Apoapsis = double.NaN.
  • Orbit.Create then picks the class from the semi-major axis when it has no state vectors (Orbit.IsParabolic(semiMajorAxis, mu) is true for +Infinity), or from the energies when it has state vectors (Orbit.GetOrbitType(orbitalEnergy, potentialEnergy), tolerance 1E-06 * (orbitalEnergy + 2 * potentialEnergy)). Both give Orbit.Parabolic for this orbit. The energy test is a little wider than the eccentricity test, so a few orbits are Parabolic with a normal period, but the whole band 1 - 1e-6 <= e < 1 is both.
So an Orbit.Parabolic can have Eccentricity < 1.0 (below 1 by less than 1e-6) and a NaN Period. Several methods use Eccentricity < 1.0 as the test for "bound orbit with a period":
  • Orbit.GetNextPeriapsisTime and Orbit.GetNextApoapsisTime: if (Eccentricity < 1.0) return TimeOfTrueAnomaly(...).
  • Orbit.GetTimeFromToTrueAnomalyAtGameTime: after it computes value = timeFromPeTo - timeSincePeriapsisThisOrbit, it does if (Eccentricity < 1.0) value -= Period * Math.Floor(value.Seconds() / Period + 0.5);. With Period = NaN the subtraction goes to UniverseTime.operator -(UniverseTime, double) and UniverseTime.NanosecondsFromSeconds, which throws on NaN.
Before that line, Orbit.GetTimeSincePeriapsisThisOrbit is safe, because it tests IsBound() (this is Bound), not the eccentricity. Vehicle.UpdatePerFrameData calls GetNextPeriapsisTime and GetNextApoapsisTime for every vehicle every frame, so the exception is thrown in the first frame in which the orbit is in the band. For e >= 1 the same methods take the other branch (TimeAtPeriapsis, gameTime), so only the side below 1 is affected. The two control runs above show both sides.

Suggested fix
Use the class test instead of the eccentricity test where a period is needed:
  • In Orbit.GetNextPeriapsisTime, Orbit.GetNextApoapsisTime and Orbit.GetTimeFromToTrueAnomalyAtGameTime, replace Eccentricity < 1.0 by IsBound() (or double.IsFinite(Period)). Then an Orbit.Parabolic takes the unbound branch, as an orbit with e >= 1 does today.
  • Optional: use one classification everywhere. Today OrbitData.IsParabolic (1e-6 on e), Orbit.IsParabolic(double) (the same) and Orbit.GetOrbitType (energy tolerance) disagree in a thin zone. A shared helper would prevent the next case of this kind, and it would also make it impossible to confuse a Parabolic orbit with e < 1 with a bound orbit.
  • A cheap safety net would be a guard in Vehicle.UpdatePerFrameData that keeps the old _nextPeriapsisTime and logs a warning instead of letting an exception reach the main loop.
 
Upvote 0
Back
Top