diff --git a/README.md b/README.md
index a14fb45..6b8601a 100644
--- a/README.md
+++ b/README.md
@@ -702,6 +702,52 @@ Example: `r.resource[ElectricCharge]`, `r.resource[LiquidFuel]`, `r.resource[Oxi
| `alarm.nextAlarm` | Next alarm to trigger |
| `alarm.timeToNext` | Time until next alarm (s) |
+### `recovery.*` / `crash.*` / `flight.*` — Mission outcomes
+
+Snapshot keys captured from `GameEvents` — readable from any scene (including Space Center / Tracking Station after the flight) so a dashboard can surface a "last outcome" panel.
+
+| Key | Description |
+|-----|-------------|
+| `recovery.lastSummary` | Last `MissionRecoveryDialog` content (vesselName, recoveryFactor, scienceEarned, fundsEarned, parts[], resources[], crew[]) + FlightLogger stats |
+| `recovery.hasRecent` | Whether a recovery snapshot is available |
+| `crash.lastCrash` | Most recent notable-vessel crash — terrain, water, or non-collision loss (burn-up / structural). Debris/flags excluded. Full object below |
+| `crash.hasRecent` | Whether a crash snapshot is available |
+| `flight.events` | Current flight's `FlightLogger.eventLog` (live) |
+| `flight.achievements` | Highest altitude / speed / G / partsLost / etc. |
+
+> `PRELAUNCH` recovery (the cheap launchpad refund path) doesn't fire `onVesselRecoveryProcessingComplete` so it produces no snapshot. Flight-scene recoveries and the post-landing summary dialog both work.
+
+crash.lastCrash — full object
+
+A single-slot "last notable crash", fed by three KSP signals: `onCrash` (terrain), `onCrashSplashdown` (water), and `onVesselWillDestroy` (a non-collision loss — re-entry burn-up, structural/aero break-up, or an impact so fast `CollisionEnhancer` destroyed the craft before a collision event fired). Persists across scenes; cleared on KSP restart.
+
+**Excluded** (never recorded): vessels of type `Debris`, `Flag`, or `Unknown` — spent boosters and the like won't clobber the slot and hide the real crash (`SpaceObject`/asteroids are kept, being pilotable). The `onVesselWillDestroy` path additionally requires the **active** vessel, a situation other than `PRELAUNCH`, and that no revert / recovery / scene change is in progress.
+
+| Field | Type | Notes |
+|-------|------|-------|
+| `eventKind` | string | `Crash` (terrain) · `CrashSplashdown` (water) · `Destroyed` (non-collision) |
+| `vesselName` | string | |
+| `vesselType` | string | KSP `VesselType` — `Ship`, `Probe`, `Lander`, `SpaceObject`, … (never Debris/Flag/Unknown) |
+| `vesselId` | string | vessel GUID |
+| `body` | string | celestial body, e.g. `Kerbin` |
+| `situation` | string | KSP situation at death — `FLYING`, `LANDED`, `SPLASHED`, … |
+| `latitude` / `longitude` / `altitude` | number | site of the event (4dp) |
+| `ut` | number | universal time |
+| `what` | string | what was struck — collision paths only; empty for `Destroyed` |
+| `msg` | string | collision message; empty for `Destroyed` |
+| `partsLost` | array | `{ partName, partTitle, partId, msg }` |
+| `crewAboard` | string[] | crew aboard at the time |
+| `kerbalsKilled` | string[] | crew killed |
+| `events` | string[] | `FlightLogger` event log (e.g. `"… exploded due to overheating: 2201 / 2200 K"`) |
+| `flightStats` | object | `FlightLogger` mission stats — `highestSpeed`, `highestAltitude`, `highestGee`, `flightEndMode`, … |
+
+Two value-shape notes:
+
+- **Collision** (`Crash` / `CrashSplashdown`): `partsLost` lists the parts that struck, coalesced over a 5 s window; `what` and `msg` are populated.
+- **`Destroyed`**: `what` and `msg` are empty, and `partsLost` is whatever parts remained at destroy time — often `[]` for a burn-up, since parts cook off individually before the vessel-destroy fires. The `events` log is then the record of what happened.
+
+
+
### `m.*` — Map view
| Key | Description |
diff --git a/Telemachus/src/CrashDataHandler.cs b/Telemachus/src/CrashDataHandler.cs
new file mode 100644
index 0000000..2da5745
--- /dev/null
+++ b/Telemachus/src/CrashDataHandler.cs
@@ -0,0 +1,405 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Telemachus
+{
+ /// Vessel-crash snapshot — coalesces per-part onCrash events within a 5s window and buffers onCrewKilled (which fires before onCrash, so kills must be staged).
+ public class CrashDataHandler : DataLinkHandler
+ {
+ private const double CoalesceWindowSeconds = 5.0;
+
+ private static Dictionary _lastCrash;
+ private static double _lastCrashUT;
+ private static string _lastCrashVesselId;
+
+ // Wall-clock (realtimeSinceStartup) of the last capture. Used ONLY to
+ // dedup the onCrash→onVesselWillDestroy pair of a single collision crash
+ // (they fire in the same frame). Game-UT can't be used for this: a
+ // revert resets it and reuses the vesselId, which made a separate later
+ // burn-up look "already captured". Wall-clock doesn't reset on revert.
+ private static float _lastCaptureRealtime = -999f;
+
+ // Suppression for the onVesselWillDestroy detector — set while a benign
+ // destruction is in progress (revert / recovery / scene change). All
+ // cleared when the next scene finishes loading, so a stuck flag can
+ // never make us miss a later real crash.
+ private static bool _reverting;
+ private static bool _recovering;
+ private static bool _sceneChanging;
+
+ // onCrewKilled fires before onCrash — buffer kill names so they don't get lost when the snapshot is built.
+ private static readonly List _pendingKerbalsKilled = new List();
+
+ // Pre-tracked because KSP flips dead kerbals to RosterStatus.Dead before onCrash, and GetVesselCrew() filters Dead out.
+ private static List _lastKnownCrew = new List();
+ private static string _lastKnownCrewVesselId;
+
+ public CrashDataHandler(FormatterProvider formatters)
+ : base(formatters) { }
+
+ private static double R4(double v) => Math.Round(v, 4);
+ private static double R4(float v) => R4((double)v);
+
+ internal static void OnCrash(EventReport report) => CaptureCrash(report, "Crash");
+ internal static void OnCrashSplashdown(EventReport report) => CaptureCrash(report, "CrashSplashdown");
+
+ // ── Vessel-destroyed detector ───────────────────────────────────────
+ // Collision crashes fire onCrash/onCrashSplashdown (handled above).
+ // Non-collision deaths — re-entry burn-up, structural/aero failure —
+ // fire NEITHER, but every fatal outcome fires onVesselWillDestroy. We use
+ // it as the universal "the mission ended by mishap" signal, scoped to the
+ // active vessel and gated against benign destroys (revert / recovery /
+ // scene change). A collision crash already captured by onCrash in the
+ // same frame is deduped, so it is not double-recorded.
+ internal static void OnVesselWillDestroy(Vessel v)
+ {
+ try
+ {
+ if (!IsRecordableMishap(v)) return;
+ CaptureVesselDestroyed(v);
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning("[Telemachus] vessel-destroy capture failed: " + e.Message);
+ }
+ }
+
+ private static bool IsRecordableMishap(Vessel v)
+ {
+ if (v == null || v != FlightGlobals.ActiveVessel) return false;
+ if (_reverting || _recovering || _sceneChanging) return false;
+ if (v.situation == Vessel.Situations.PRELAUNCH) return false;
+ var vType = v.vesselType;
+ if (vType == VesselType.Debris || vType == VesselType.Flag || vType == VesselType.Unknown)
+ return false;
+ // Dedup the same collision crash onCrash just captured — those fire in
+ // the same frame, so use WALL-CLOCK, not game-UT (a revert resets game-
+ // UT while reusing the vesselId, which would falsely match a separate
+ // later crash). A burn-up minutes later won't fall in this window.
+ if (_lastCrash != null && _lastCrashVesselId == v.id.ToString()
+ && Time.realtimeSinceStartup - _lastCaptureRealtime < 2f)
+ return false;
+ return true;
+ }
+
+ private static void CaptureVesselDestroyed(Vessel v)
+ {
+ double ut = Planetarium.GetUniversalTime();
+ var partsLost = new List>();
+ if (v.parts != null)
+ {
+ foreach (var p in v.parts)
+ {
+ if (p == null) continue;
+ partsLost.Add(new Dictionary
+ {
+ ["partName"] = p.partInfo?.name ?? p.name ?? string.Empty,
+ ["partTitle"] = p.partInfo?.title ?? string.Empty,
+ ["partId"] = p.flightID,
+ ["msg"] = string.Empty,
+ });
+ }
+ }
+ var crewAboard = _lastKnownCrew != null && _lastKnownCrew.Count > 0
+ ? new List(_lastKnownCrew)
+ : ListCrewAboard(v);
+ var kerbalsKilled = new List(_pendingKerbalsKilled);
+ _pendingKerbalsKilled.Clear();
+ var snap = new Dictionary
+ {
+ // "Destroyed" = non-collision loss (burn-up / structural). The
+ // banner shows VESSEL DESTROYED for any eventKind; this just
+ // distinguishes it from terrain (Crash) / water (CrashSplashdown).
+ ["eventKind"] = "Destroyed",
+ ["vesselName"] = v.vesselName ?? string.Empty,
+ ["vesselType"] = v.vesselType.ToString(),
+ ["vesselId"] = v.id.ToString(),
+ ["body"] = v.mainBody?.bodyName ?? string.Empty,
+ ["situation"] = v.situation.ToString(),
+ ["latitude"] = R4(v.latitude),
+ ["longitude"] = R4(v.longitude),
+ ["altitude"] = R4(v.altitude),
+ ["ut"] = R4(ut),
+ ["what"] = string.Empty,
+ ["msg"] = string.Empty,
+ ["partsLost"] = partsLost,
+ ["crewAboard"] = crewAboard,
+ ["kerbalsKilled"] = kerbalsKilled,
+ ["events"] = FlightLoggerSnapshot.CaptureEvents(),
+ ["flightStats"] = FlightLoggerSnapshot.Capture(),
+ };
+ _lastCrash = snap;
+ _lastCrashUT = ut;
+ _lastCrashVesselId = v.id.ToString();
+ _lastCaptureRealtime = Time.realtimeSinceStartup;
+ }
+
+ // Suppression bookkeeping — benign destroys must not register as crashes.
+ internal static void NoteRevert() => _reverting = true;
+ internal static void NoteRecoveryRequested() => _recovering = true;
+ internal static void NoteVesselRecovered() => _recovering = false;
+ internal static void NoteSceneLoad() => _sceneChanging = true;
+ internal static void NoteLevelLoaded()
+ {
+ _reverting = false;
+ _recovering = false;
+ _sceneChanging = false;
+ }
+
+ internal static void OnCrewKilled(EventReport report)
+ {
+ try
+ {
+ var name = report?.sender ?? string.Empty;
+ if (string.IsNullOrEmpty(name)) return;
+ if (!_pendingKerbalsKilled.Contains(name)) _pendingKerbalsKilled.Add(name);
+ if (_lastCrash != null
+ && _lastCrash.TryGetValue("kerbalsKilled", out var raw)
+ && raw is List list
+ && !list.Contains(name))
+ {
+ list.Add(name);
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning("[Telemachus] crash crew capture failed: " + e.Message);
+ }
+ }
+
+ /// Sampled by the addon every ~0.5s — pre-crash crew, kept fresh because KSP wipes the live list before onCrash fires.
+ internal static void RefreshLiveCrew(string vesselId, List crew)
+ {
+ if (vesselId == null) return;
+ // Non-empty wins on same vessel: a mid-crash wipe must not overwrite the last good sample.
+ if (vesselId != _lastKnownCrewVesselId)
+ {
+ _lastKnownCrewVesselId = vesselId;
+ _lastKnownCrew = new List(crew);
+ return;
+ }
+ if (crew.Count > 0)
+ {
+ _lastKnownCrew = new List(crew);
+ }
+ }
+
+ private static void CaptureCrash(EventReport report, string eventKind)
+ {
+ if (report == null) return;
+ try
+ {
+ var origin = report.origin;
+ var vessel = origin?.vessel;
+ var vesselId = vessel?.id.ToString() ?? string.Empty;
+ var ut = Planetarium.GetUniversalTime();
+
+ // crash.lastCrash is a single-slot "last notable crash" record,
+ // not a raw impact feed. Spent debris (decoupled boosters/
+ // stages), planted flags, and unclassified objects aren't a
+ // vessel the operator is flying — recording them would clobber
+ // the slot and lose the real crash. Drop them at the source so
+ // every consumer (banner, flight-history annotation, reconnect
+ // replay) sees the right value. SpaceObject/asteroids are
+ // pilotable, so they stay.
+ var vType = vessel?.vesselType;
+ if (vType == VesselType.Debris
+ || vType == VesselType.Flag
+ || vType == VesselType.Unknown)
+ {
+ return;
+ }
+
+ // Same vessel within window → append to existing snapshot.
+ if (_lastCrash != null
+ && _lastCrashVesselId == vesselId
+ && ut - _lastCrashUT < CoalesceWindowSeconds)
+ {
+ AppendPart(_lastCrash, origin, report);
+ _lastCrashUT = ut;
+ _lastCaptureRealtime = Time.realtimeSinceStartup;
+ return;
+ }
+
+ var crewAboard = _lastKnownCrew != null && _lastKnownCrew.Count > 0
+ ? new List(_lastKnownCrew)
+ : ListCrewAboard(vessel);
+ var kerbalsKilled = new List(_pendingKerbalsKilled);
+ _pendingKerbalsKilled.Clear();
+ var snap = new Dictionary
+ {
+ ["eventKind"] = eventKind,
+ ["vesselName"] = vessel?.vesselName ?? report.sender ?? string.Empty,
+ ["vesselType"] = vessel?.vesselType.ToString() ?? string.Empty,
+ ["vesselId"] = vesselId,
+ ["body"] = vessel?.mainBody?.bodyName ?? string.Empty,
+ ["situation"] = vessel?.situation.ToString() ?? string.Empty,
+ ["latitude"] = R4(vessel?.latitude ?? 0.0),
+ ["longitude"] = R4(vessel?.longitude ?? 0.0),
+ ["altitude"] = R4(vessel?.altitude ?? 0.0),
+ ["ut"] = R4(ut),
+ ["what"] = report.other ?? string.Empty,
+ ["msg"] = report.msg ?? string.Empty,
+ ["partsLost"] = new List>(),
+ ["crewAboard"] = crewAboard,
+ ["kerbalsKilled"] = kerbalsKilled,
+ ["events"] = FlightLoggerSnapshot.CaptureEvents(),
+ ["flightStats"] = FlightLoggerSnapshot.Capture(),
+ };
+ AppendPart(snap, origin, report);
+ _lastCrash = snap;
+ _lastCrashUT = ut;
+ _lastCrashVesselId = vesselId;
+ _lastCaptureRealtime = Time.realtimeSinceStartup;
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning("[Telemachus] crash capture failed: " + e.Message);
+ }
+ }
+
+ private static void AppendPart(Dictionary snap, Part origin, EventReport report)
+ {
+ if (snap == null || origin == null) return;
+ if (!snap.TryGetValue("partsLost", out var raw)) return;
+ if (raw is not List> list) return;
+ list.Add(new Dictionary
+ {
+ ["partName"] = origin.partInfo?.name ?? origin.name ?? string.Empty,
+ ["partTitle"] = origin.partInfo?.title ?? string.Empty,
+ ["partId"] = origin.flightID,
+ ["msg"] = report.msg ?? string.Empty,
+ });
+ }
+
+ private static List ListCrewAboard(Vessel vessel)
+ {
+ var result = new List();
+ if (vessel == null) return result;
+ try
+ {
+ var crew = vessel.GetVesselCrew();
+ if (crew == null) return result;
+ foreach (var member in crew)
+ {
+ if (member?.name != null) result.Add(member.name);
+ }
+ }
+ catch (Exception)
+ {
+ // Protected vessels in odd states can throw — treat as no crew.
+ }
+ return result;
+ }
+
+ [TelemetryAPI("crash.hasRecent",
+ "Whether a crash snapshot has been captured this session.",
+ AlwaysEvaluable = true,
+ Category = "crash",
+ ReturnType = "bool")]
+ object HasRecent(DataSources ds) => _lastCrash != null;
+
+ [TelemetryAPI("crash.lastCrash",
+ "Most recent notable-vessel crash snapshot. Captures terrain " +
+ "(eventKind Crash), water (CrashSplashdown), and non-collision " +
+ "losses such as re-entry burn-up or structural break-up " +
+ "(Destroyed). Debris, flags, and unclassified vessels are " +
+ "excluded so they don't overwrite the last real crash. Fields: " +
+ "vesselName, vesselType (KSP VesselType e.g. Ship/Probe/" +
+ "SpaceObject), vesselId, body, situation, latitude, longitude, " +
+ "altitude, ut, what (what was hit; empty for Destroyed), msg, " +
+ "eventKind (Crash/CrashSplashdown/Destroyed), partsLost (list " +
+ "of {partName, partTitle, partId, msg}), crewAboard (names), " +
+ "kerbalsKilled (names), events (flight log), flightStats. " +
+ "Per-vessel collision events within 5 seconds coalesce into one " +
+ "snapshot. Persists across scene changes; cleared on KSP restart.",
+ AlwaysEvaluable = true,
+ Plotable = false,
+ Category = "crash",
+ ReturnType = "object")]
+ object LastCrash(DataSources ds) => _lastCrash;
+ }
+
+ /// Deferred subscriber — instance handlers required because EvtDelegate ctor reads Target.GetType().
+ [KSPAddon(KSPAddon.Startup.MainMenu, true)]
+ public class CrashDataSubscriber : MonoBehaviour
+ {
+ private static bool _subscribed;
+
+ private float _crewSampleAccumulator;
+ private const float CrewSampleIntervalSeconds = 0.5f;
+ private readonly List _crewScratch = new List();
+
+ private void Awake()
+ {
+ DontDestroyOnLoad(this);
+ if (_subscribed) return;
+ try
+ {
+ GameEvents.onCrash.Add(HandleCrash);
+ GameEvents.onCrashSplashdown.Add(HandleCrashSplashdown);
+ GameEvents.onCrewKilled.Add(HandleCrewKilled);
+
+ // Universal mishap signal for non-collision deaths (burn-up,
+ // structural) that fire no onCrash. Gated against benign
+ // destroys by the revert/recovery/scene-load subscriptions.
+ GameEvents.onVesselWillDestroy.Add(HandleVesselWillDestroy);
+ GameEvents.OnRevertToLaunchFlightState.Add(HandleRevertLaunch);
+ GameEvents.OnRevertToPrelaunchFlightState.Add(HandleRevertPrelaunch);
+ GameEvents.OnVesselRecoveryRequested.Add(HandleRecoveryRequested);
+ GameEvents.onVesselRecovered.Add(HandleVesselRecovered);
+ GameEvents.onGameSceneLoadRequested.Add(HandleSceneLoadRequested);
+ GameEvents.onLevelWasLoaded.Add(HandleLevelLoaded);
+
+ _subscribed = true;
+ Debug.Log("[Telemachus] CrashDataSubscriber: subscribed to onCrash/onCrashSplashdown/onCrewKilled + onVesselWillDestroy (+ suppression)");
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning(
+ "[Telemachus] CrashDataSubscriber: subscription failed; " +
+ "crash.lastCrash will stay empty this session: " + e.Message);
+ }
+ }
+
+ private void Update()
+ {
+ _crewSampleAccumulator += Time.unscaledDeltaTime;
+ if (_crewSampleAccumulator < CrewSampleIntervalSeconds) return;
+ _crewSampleAccumulator = 0f;
+ try
+ {
+ var vessel = FlightGlobals.ActiveVessel;
+ if (vessel == null) return;
+ _crewScratch.Clear();
+ var crew = vessel.GetVesselCrew();
+ if (crew != null)
+ {
+ foreach (var member in crew)
+ {
+ if (member?.name != null) _crewScratch.Add(member.name);
+ }
+ }
+ CrashDataHandler.RefreshLiveCrew(vessel.id.ToString(), _crewScratch);
+ }
+ catch (Exception)
+ {
+ // Scene transitions can throw inside GetVesselCrew() — keep the last-known sample.
+ }
+ }
+
+ private void HandleCrash(EventReport r) => CrashDataHandler.OnCrash(r);
+ private void HandleCrashSplashdown(EventReport r) => CrashDataHandler.OnCrashSplashdown(r);
+ private void HandleCrewKilled(EventReport r) => CrashDataHandler.OnCrewKilled(r);
+
+ // Instance handlers (EvtDelegate needs a real Target.GetType()).
+ private void HandleVesselWillDestroy(Vessel v) => CrashDataHandler.OnVesselWillDestroy(v);
+ private void HandleRevertLaunch(FlightState s) => CrashDataHandler.NoteRevert();
+ private void HandleRevertPrelaunch(FlightState s) => CrashDataHandler.NoteRevert();
+ private void HandleRecoveryRequested(Vessel v) => CrashDataHandler.NoteRecoveryRequested();
+ private void HandleVesselRecovered(ProtoVessel pv, bool quick) => CrashDataHandler.NoteVesselRecovered();
+ private void HandleSceneLoadRequested(GameScenes s) => CrashDataHandler.NoteSceneLoad();
+ private void HandleLevelLoaded(GameScenes s) => CrashDataHandler.NoteLevelLoaded();
+ }
+}
diff --git a/Telemachus/src/FlightLogHandler.cs b/Telemachus/src/FlightLogHandler.cs
new file mode 100644
index 0000000..4791d63
--- /dev/null
+++ b/Telemachus/src/FlightLogHandler.cs
@@ -0,0 +1,32 @@
+namespace Telemachus
+{
+ /// Read-access to KSP's FlightLogger — same data the in-game Flight Results dialog renders. AlwaysEvaluable because the snapshot helper degrades gracefully outside flight.
+ public class FlightLogHandler : DataLinkHandler
+ {
+ public FlightLogHandler(FormatterProvider formatters)
+ : base(formatters) { }
+
+ [TelemetryAPI("flight.events",
+ "Pre-formatted event timeline from KSP's FlightLogger — the same " +
+ "strings the in-game Flight Results dialog renders in the Flight " +
+ "Events panel. Empty list outside flight.",
+ AlwaysEvaluable = true,
+ Plotable = false,
+ Category = "flight",
+ ReturnType = "object")]
+ object Events(DataSources ds) => FlightLoggerSnapshot.CaptureEvents();
+
+ [TelemetryAPI("flight.achievements",
+ "Running mission stats from KSP's FlightLogger — same data the " +
+ "Flight Results dialog renders in the Flight Achievements panel. " +
+ "Fields: missionTime, liftOff, highestAltitude, highestSpeed, " +
+ "highestSpeedOverLand, groundDistance, totalDistance, highestGee, " +
+ "partsLost (count), kerbalsKilled (count), missionEnd, " +
+ "flightEndMode. Returns null outside flight.",
+ AlwaysEvaluable = true,
+ Plotable = false,
+ Category = "flight",
+ ReturnType = "object")]
+ object Achievements(DataSources ds) => FlightLoggerSnapshot.Capture();
+ }
+}
diff --git a/Telemachus/src/FlightLoggerSnapshot.cs b/Telemachus/src/FlightLoggerSnapshot.cs
new file mode 100644
index 0000000..da24796
--- /dev/null
+++ b/Telemachus/src/FlightLoggerSnapshot.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using UnityEngine;
+
+namespace Telemachus
+{
+ /// Shared snapshot helper for FlightLogger — most fields are private instance fields, accessed via cached reflection.
+ public static class FlightLoggerSnapshot
+ {
+ private static bool _reflectionResolved;
+ private static FieldInfo _highestAltitude;
+ private static FieldInfo _groundDistance;
+ private static FieldInfo _totalDistance;
+ private static FieldInfo _highestGee;
+ private static FieldInfo _highestSpeed;
+ private static FieldInfo _highestSpeedOverLand;
+ private static FieldInfo _liftOff;
+ private static FieldInfo _missionEnd;
+ private static FieldInfo _partsLost;
+ private static FieldInfo _kerbalsKilled;
+ private static FieldInfo _flightEndMode;
+
+ private static void EnsureReflection()
+ {
+ if (_reflectionResolved) return;
+ var t = typeof(FlightLogger);
+ const BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Instance;
+ _highestAltitude = t.GetField("highestAltitude", bf);
+ _groundDistance = t.GetField("groundDistance", bf);
+ _totalDistance = t.GetField("totalDistance", bf);
+ _highestGee = t.GetField("highestGee", bf);
+ _highestSpeed = t.GetField("highestSpeed", bf);
+ _highestSpeedOverLand = t.GetField("highestSpeedOverLand", bf);
+ _liftOff = t.GetField("liftOff", bf);
+ _missionEnd = t.GetField("missionEnd", bf);
+ _partsLost = t.GetField("partsLost", bf);
+ _kerbalsKilled = t.GetField("kerbalsKilled", bf);
+ _flightEndMode = t.GetField("flightEndMode", bf);
+ _reflectionResolved = true;
+ }
+
+ private static double R4(double v) => Math.Round(v, 4);
+
+ private static T ReadField(FieldInfo f, FlightLogger logger, T fallback)
+ {
+ if (f == null || logger == null) return fallback;
+ try
+ {
+ var v = f.GetValue(logger);
+ if (v is T typed) return typed;
+ return fallback;
+ }
+ catch (Exception)
+ {
+ return fallback;
+ }
+ }
+
+ /// Snapshot current FlightLogger state — returns null when FlightLogger.fetch isn't available (no flight scene).
+ public static Dictionary Capture()
+ {
+ try
+ {
+ var logger = FlightLogger.fetch;
+ if (logger == null) return null;
+ EnsureReflection();
+ return new Dictionary
+ {
+ ["missionTime"] = R4(FlightLogger.met),
+ ["liftOff"] = FlightLogger.LiftOff,
+ ["highestAltitude"] = R4(ReadField(_highestAltitude, logger, 0.0)),
+ ["highestSpeed"] = R4(ReadField(_highestSpeed, logger, 0.0)),
+ ["highestSpeedOverLand"] = R4(ReadField(_highestSpeedOverLand, logger, 0.0)),
+ ["groundDistance"] = R4(ReadField(_groundDistance, logger, 0.0)),
+ ["totalDistance"] = R4(ReadField(_totalDistance, logger, 0.0)),
+ ["highestGee"] = R4(ReadField(_highestGee, logger, 0.0)),
+ ["partsLost"] = ReadField(_partsLost, logger, 0),
+ ["kerbalsKilled"] = ReadField(_kerbalsKilled, logger, 0),
+ ["missionEnd"] = ReadField(_missionEnd, logger, false),
+ ["flightEndMode"] =
+ ReadField