Kitten Space Agency mod to load real Artemis II orbit data
1using System.Runtime.InteropServices;
2using Brutal.ImGuiApi;
3using Brutal.Numerics;
4using KSA;
5
6using static Brutal.Logging.DefaultCategory;
7
8namespace OemLoader;
9
10public class MainWindow(string title)
11{
12 public bool Typing { get; private set; }
13 public bool Open;
14 public bool DrawRealTrajectory;
15
16 private readonly ImInputString _filename = new(short.MaxValue);
17 public EphemerisTrajectory? Ephemeris { get; private set; }
18 private string? _errorMessage;
19 private long _sliderTicks;
20
21 // Assume the ephemeris is for the home body of the system (i.e. Earth), fall back to vehicle's current parent
22 // if null
23 public IParentBody? ParentBody => Universe.CurrentSystem?.HomeBody ?? Program.ControlledVehicle?.Parent;
24
25
26 private void GoToTime(DateTime time)
27 {
28 if (Program.ControlledVehicle is null || Ephemeris is not EphemerisTrajectory oem) return;
29 var body = ParentBody;
30 if (body is null) return;
31
32 var orb = oem.OrbitForTime(time, body, Program.ControlledVehicle.Orbit.OrbitLineColor);
33 if (orb is null) return;
34 var simTime = new SimTime((time - OemLoader.KsaEpoch).TotalSeconds);
35
36 // If the position at the provided time is within the SOI of a child body, transform the orbit to
37 // an orbit around the child body.
38 // (Code mostly stolen from KSA.KinematicStates.CheckSoiTransitions)
39 var posCci = orb.GetStateVectorsAt(simTime).PositionCci;
40 foreach (var child in body.Children)
41 {
42 if (child is not Celestial childCelestial) continue;
43 var soi = childCelestial.SphereOfInfluence;
44 var childPos = child.GetPositionOrb(simTime).Transform(childCelestial.Orbit.GetOrb2ParentCci());
45 var distance = (posCci - childPos).Length();
46
47 if (distance > soi) continue;
48 var rotation = doubleQuat.Concatenate(childCelestial.GetCci2Orb(), childCelestial.Orbit.Orb2ParentCci)
49 .Inverse();
50 var newPosition = (posCci - childPos).Transform(rotation);
51 var newVelocity = (orb.GetVelocityCciAt(simTime) - childCelestial.GetVelocityCci(simTime)).Transform(rotation);
52 orb = Orbit.CreateFromStateCci(childCelestial, simTime, newPosition, newVelocity, orb.OrbitLineColor);
53 }
54
55 // Go to the specified time (thanks to UniversePatch.ApplyVehicleSolversTranspiler this works
56 // even for negative values of dt)
57 var dt = simTime.Seconds() - Universe.GetElapsedSeconds();
58 // This is similar to the implementation of the stock `simulate` console command, but we
59 // reset the simulation speed to 1x to avoid the bug in that command where the time step
60 // gets multiplied by the simulation speed
61 var step = Universe.GetJobSimStep(dt);
62 Universe.ApplyVehicleSolvers();
63 Universe.ApplyOrbitSolvers();
64 Universe.ResetSimulationSpeed();
65 Universe.ExecuteNextOrbitSolvers(dt, step);
66 Universe.ExecuteNextVehicleSolvers(dt, step);
67 Universe.ApplyVehicleSolvers();
68 Universe.ApplyOrbitSolvers();
69 // Finally, teleport the vehicle onto the orbit calculated from the ephemeris
70 Program.ControlledVehicle.Teleport(orb, null, null);
71 Log.Debug($"OemLoader: Jumped to time {time} from ephemeris");
72 }
73
74 public void Render()
75 {
76 Typing = false;
77 if (!Open || !Program.DrawUI) return;
78
79 ImGui.SetNextWindowSize(new float2(500, 300), ImGuiCond.Once);
80 ImGui.SetNextWindowSizeConstraints(new float2(200), new float2(2000));
81 if (ImGui.Begin(title, ref Open))
82 {
83 var windows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
84 ImGui.InputTextWithHint("OEM file path", windows ? @"C:\Users\User\OEM.asc" : "/home/user/OEM.asc", _filename);
85
86 if (ImGui.Button("Load OEM file"))
87 {
88 try
89 {
90 Log.Debug($"OemLoader: Attempting to load ephemeris from {_filename.ToString()}...");
91 Ephemeris = EphemerisTrajectory.ReadFile(_filename.ToString());
92 if (Ephemeris is EphemerisTrajectory oem)
93 {
94 _sliderTicks = oem.StartTime.Ticks;
95 _errorMessage = null;
96 Log.Info($"OemLoader: Loaded ephemeris from {_filename.ToString()}"
97 + $" ({oem.Count} entries from {oem.StartTime} to {oem.EndTime})");
98 }
99 else
100 {
101 _errorMessage = "Could not find any ephemeris data in file";
102 Log.Warning($"OemLoader: Could not find any ephemeris data in {_filename.ToString()}");
103 }
104 }
105 catch (Exception e)
106 {
107 _errorMessage = $"Failed to open file:\n{e}";
108 Log.Warning($"OemLoader: Failed to open ephemeris file {_filename.ToString()}:\n{e}");
109 }
110 }
111
112 if (_errorMessage is not null)
113 {
114 ImGui.TextColored(new float4(0.863f, 0.196f, 0.184f, 1f), _errorMessage);
115 }
116
117 if (Ephemeris is EphemerisTrajectory traj && Program.ControlledVehicle is not null)
118 {
119 ImGui.Text($"Loaded ephemeris valid from {traj.StartTime} to {traj.EndTime}");
120
121 ImGui.Checkbox("Draw trajectory", ref DrawRealTrajectory);
122
123 var now = DateTime.UtcNow;
124 if (traj.StartTime <= now && now <= traj.EndTime)
125 {
126 if (ImGui.Button("Go to now"))
127 GoToTime(now);
128 }
129
130 var minTicks = traj.StartTime.Ticks;
131 var maxTicks = traj.EndTime.Ticks;
132 unsafe
133 {
134 fixed (long* pTicks = &_sliderTicks)
135 ImGui.SliderScalar(
136 "Time",
137 ImGuiDataType.U64,
138 pTicks,
139 &minTicks,
140 &maxTicks,
141 "",
142 ImGuiSliderFlags.AlwaysClamp | ImGuiSliderFlags.NoRoundToFormat
143 );
144 }
145
146 var sliderTime = new DateTime(_sliderTicks);
147 if (ImGui.Button($"Go to {sliderTime}"))
148 GoToTime(sliderTime);
149 }
150
151 Typing = ImGui.IsWindowFocused() && ImGui.GetIO().WantCaptureKeyboard;
152 }
153 ImGui.End();
154 }
155}